# CKEditor 5 source file: "ckeditor5/latest/examples/builds-custom/collaborative-document-editor.html" ## Collaborative document editor CKEditor 5 is a highly flexible framework that was created with collaboration in mind. It lets you build your custom editor of any type, with any set of [features](#ckeditor5/latest/features/index.html) and the [toolbar](#ckeditor5/latest/getting-started/setup/toolbar.html) type that you need where multiple authors can easily work on the same rich text documents. This feature-rich editor also brings many edition- and management-enhancing tools to the table, turning it into a full-fledged editing experience comparable with the most popular solutions such as Google Docs or Microsoft Word. By default, CKEditor 5 filters out any content that is unsupported by its plugins and configuration. Check out the [General HTML Support (“GHS”)](#ckeditor5/latest/features/html/general-html-support.html) feature that allows you to enable HTML features that are not explicitly supported by any other dedicated CKEditor 5 plugins. source file: "ckeditor5/latest/examples/builds-custom/full-featured-editor.html" ## Feature-rich editor CKEditor 5 is a highly flexible framework that lets you build your custom editor of any type (like [classic](#ckeditor5/latest/examples/builds/classic-editor.html), [inline](#ckeditor5/latest/examples/builds/inline-editor.html), [distraction-free](#ckeditor5/latest/examples/builds/balloon-block-editor.html), or [document-like](#ckeditor5/latest/examples/builds/document-editor.html)), with any set of [features](#ckeditor5/latest/features/index.html) and the [toolbar](#ckeditor5/latest/getting-started/setup/toolbar.html) type that you need [in no time](https://ckeditor.com/ckeditor-5/builder/?redirect=docs). This custom editor preset contains almost all non-collaborative CKEditor 5 features. You can use it to create your own content or to paste some existing content from Microsoft Word, Google Docs, text documents, or any online resources. This editor was configured specifically to allow testing as many features as possible in one demo, with a multiline toolbar (with some features grouped into dropdowns) that gives you easy access to all available features. It is based on the classic editor, providing you with a boxed editing area with a toolbar, placed in a specific position on the page. The toolbar has been specially configured to host as many features as possible in a convenient setup. CKEditor 5 offers a dedicated [accessibility help dialog](#ckeditor5/latest/features/accessibility.html--displaying-keyboard-shortcuts-in-the-editor) that displays a list of all available keyboard shortcuts in a dialog. It can be opened by pressing Alt \+ 0 (on Windows) or Option \+ 0 (on macOS) or via toolbar. Thanks to the [autoformatting](#ckeditor5/latest/features/autoformat.html) feature you can also use Markdown-like inline shortcodes as you type to create and format your content without using the toolbar buttons. The [slash command](#ckeditor5/latest/features/slash-commands.html) feature lets you format and insert content on the go. You can also see the [collaborative document editor](#ckeditor5/latest/examples/builds-custom/collaborative-document-editor.html), to try out features such as comments, comments archive, track changes, or revision history, and other features enhancing document editing functions. By default, CKEditor 5 filters out any content that is unsupported by its plugins and configuration. Check out the [General HTML Support (“GHS”)](#ckeditor5/latest/features/html/general-html-support.html) feature that allows you to enable HTML features that are not explicitly supported by any other dedicated CKEditor 5 plugins. While this demo has the [import from Word](#ckeditor5/latest/features/converters/import-word/import-word.html) feature enabled, please consider that the comments and track changes features are not enabled and hence these elements will not show up in the content. Read more about handling such situations in the import from Word’s [features comparison](#ckeditor5/latest/features/converters/import-word/features-comparison.html--collaboration-features) guide. You can test these features working together in the [official import from Word demo](https://ckeditor.com/import-from-word/demo/). source file: "ckeditor5/latest/examples/builds/balloon-block-editor.html" ## Balloon block editor The balloon block editor type lets you create your content directly in its target location with the help of two toolbars: * A balloon toolbar that appears next to the selected editable document element (offering inline content formatting tools). * A [block toolbar](#ckeditor5/latest/getting-started/setup/toolbar.html--block-toolbar) accessible using the toolbar handle button attached to the editable content area and following the selection in the document (bringing additional block formatting tools). The button is also a handle that can be used to drag and drop blocks around the content. source file: "ckeditor5/latest/examples/builds/balloon-editor.html" ## Balloon editor The balloon editor type lets you create your content directly in its target location with the help of a balloon toolbar that appears next to the selected editable document element. source file: "ckeditor5/latest/examples/builds/classic-editor.html" ## Classic editor The classic editor type shows a boxed editing area with a toolbar, placed in a specific position on the page. source file: "ckeditor5/latest/examples/builds/document-editor.html" ## Document editor The editor in this example is a feature–rich preset focused on rich text editing experience similar to the native word processors. It works best for creating documents which are usually later printed or exported to PDF files. See the [tutorial](#ckeditor5/latest/framework/deep-dive/ui/document-editor.html) to learn how to create this kind of an editor (and similar) with a custom UI layout on top of [DecoupledEditor](../../api/module%5Feditor-decoupled%5Fdecouplededitor-DecoupledEditor.html). source file: "ckeditor5/latest/examples/builds/inline-editor.html" ## Inline editor The inline editor type lets you create your content directly in its target location with the help of a floating toolbar that appears when the editable text is focused. In this example the [image styles](#ckeditor5/latest/features/images/images-styles.html) configuration was changed to enable left- and right-aligned images. source file: "ckeditor5/latest/examples/builds/multi-root-editor.html" ## Multi-root editor The multi-root editor type is an editor type that features multiple, separate editable areas. The main difference between using a multi-root editor and using multiple separate editors (like in the [inline editor demo](#ckeditor5/latest/examples/builds/inline-editor.html)) is the fact that in a multi-root editor all editable areas belong to the same editor instance share the same configuration, toolbar and the undo stack, and produce one document. ### Editor example configuration Check out the [Editor types](#ckeditor5/latest/getting-started/setup/editor-types.html--multi-root-editor) guide to learn more about implementing this kind of editor. You will find implementation steps there. You can see this example editor’s code below. View editor configuration script ``` import { MultiRootEditor, Essentials, Bold, Italic, Heading, Link, Table, MediaEmbed, List, Indent } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; MultiRootEditor .create( { licenseKey: 'GPL', // Or ''. plugins: [ Essentials, Heading, Bold, Italic, Link, Table, MediaEmbed, List, Indent ], toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'insertTable', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, roots: { header: { element: document.querySelector( '#header' ), }, content: { element: document.querySelector( '#content' ), }, leftSide: { element: document.querySelector( '#left-side' ), }, rightSide: { element: document.querySelector( '#right-side' ) } } } ) .then( editor => { window.editor = editor; // Append toolbar to a proper container. const toolbarContainer = document.querySelector( '#toolbar' ); toolbarContainer.appendChild( editor.ui.view.toolbar.element ); // Make toolbar sticky when the editor is focused. editor.ui.focusTracker.on( 'change:isFocused', () => { if ( editor.ui.focusTracker.isFocused ) { toolbarContainer.classList.add( 'sticky' ); } else { toolbarContainer.classList.remove( 'sticky' ); } } ); } ) .catch( error => { console.error( 'There was a problem initializing the editor.', error ); } ); ``` View editor content listing ```
Main content is inserted here.
Left-side box content is inserted here.
Right-side box content is inserted here.
```
### Setting and reading editor data Please note that setting and reading the editor data is different for multi-root editor. Pass an object when setting the editor data Setting the data using `editor.setData()`: ``` editor.setData( { header: '

Content for header part.

', content: '

Content for main part.

', leftSide: '

Content for left-side box.

', rightSide: '

Content for right-side box.

' } ); ``` Setting the data through `config.roots..initialData`: ``` MultiRootEditor.create( { roots: { header: { initialData: '

Content for header part.

', element: document.querySelector( '#header' ) }, content: { initialData: '

Content for main part.

', element: document.querySelector( '#content' ) }, leftSide: { initialData: '

Content for left-side box.

', element: document.querySelector( '#left-side' ) }, rightSide: { initialData: '

Content for right-side box.

', element: document.querySelector( '#right-side' ) } } } ); ``` Specify root name when obtaining the data ``` editor.getData( { rootName: 'leftSide' } ); // -> '

Content for left-side box.

' ``` Learn more about using the multi-root editor in its [API documentation](../../api/module%5Feditor-multi-root%5Fmultirooteditor-MultiRootEditor.html).
source file: "ckeditor5/latest/examples/framework/bottom-toolbar-editor.html" ## Editor with a bottom toolbar and button grouping The following custom editor example showcases an editor instance with the main toolbar displayed at the bottom of the editing window. To make it possible, this example uses the [DecoupledEditor](../../api/module%5Feditor-decoupled%5Fdecouplededitor-DecoupledEditor.html) with the [main toolbar](../../api/module%5Feditor-decoupled%5Fdecouplededitoruiview-DecoupledEditorUIView.html#member-toolbar) injected after the editing root into the DOM. Learn more about the [decoupled UI in CKEditor 5](#ckeditor5/latest/framework/deep-dive/ui/document-editor.html) to find out the details of this process. Additionally, thanks to the flexibility offered by the [CKEditor 5 UI framework](#ckeditor5/latest/framework/architecture/ui-library.html), the main toolbar has been uncluttered by moving buttons related to text formatting into the custom “Formatting options” dropdown. All remaining dropdown and (button) tooltips have been tuned to open upward for the best user experience. Similar effect can also be achieved by using the [built-in toolbar grouping option](#ckeditor5/latest/getting-started/setup/toolbar.html--grouping-toolbar-items-in-dropdowns-nested-toolbars). The presented combination of the UI and editor’s features works best for integrations where text creation comes first and formatting is applied occasionally. Some examples are email applications, (forum) post editors, chats, or instant messaging. You can probably recognize this UI setup from popular applications such as Gmail, Slack, or Zendesk. ### Editor example configuration View editor configuration script ``` import { DecoupledEditor, Plugin, Alignment, Autoformat, Bold, Italic, Strikethrough, Subscript, Superscript, Underline, BlockQuote, clickOutsideHandler, Essentials, Font, Heading, HorizontalLine, Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, RemoveFormat, Table, TableToolbar, DropdownButtonView, DropdownPanelView, DropdownView, ToolbarView, IconFontColor, registerIcon } from 'ckeditor5'; const fontColorIcon =/* #__PURE__ */ registerIcon( 'fontColor', IconFontColor ); class FormattingOptions extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'FormattingOptions'; } /** * @inheritDoc */ constructor( editor ) { super( editor ); editor.ui.componentFactory.add( 'formattingOptions', locale => { const t = locale.t; const buttonView = new DropdownButtonView( locale ); const panelView = new DropdownPanelView( locale ); const dropdownView = new DropdownView( locale, buttonView, panelView ); const toolbarView = this.toolbarView = dropdownView.toolbarView = new ToolbarView( locale ); // Accessibility: Give the toolbar a human-readable ARIA label. toolbarView.set( { ariaLabel: t( 'Formatting options toolbar' ) } ); // Accessibility: Give the dropdown a human-readable ARIA label. dropdownView.set( { label: t( 'Formatting options' ) } ); // Toolbars in dropdowns need specific styling, hence the class. dropdownView.extendTemplate( { attributes: { class: [ 'ck-toolbar-dropdown' ] } } ); // Accessibility: If the dropdown panel is already open, the arrow down key should focus the first child of the #panelView. dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => { if ( dropdownView.isOpen ) { toolbarView.focus(); cancel(); } } ); // Accessibility: If the dropdown panel is already open, the arrow up key should focus the last child of the #panelView. dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => { if ( dropdownView.isOpen ) { toolbarView.focusLast(); cancel(); } } ); // The formatting options should not close when the user clicked: // * the dropdown or it contents, // * any editing root, // * any floating UI in the "body" collection // It should close, for instance, when another (main) toolbar button was pressed, though. dropdownView.on( 'render', () => { clickOutsideHandler( { emitter: dropdownView, activator: () => dropdownView.isOpen, callback: () => { dropdownView.isOpen = false; }, contextElements: [ dropdownView.element, ...[ ...editor.ui.getEditableElementsNames() ].map( name => editor.ui.getEditableElement( name ) ), document.querySelector( '.ck-body-wrapper' ) ] } ); } ); // The main button of the dropdown should be bound to the state of the dropdown. buttonView.bind( 'isOn' ).to( dropdownView, 'isOpen' ); buttonView.bind( 'isEnabled' ).to( dropdownView ); // Using the font color icon to visually represent the formatting. buttonView.set( { tooltip: t( 'Formatting options' ), icon: fontColorIcon() } ); dropdownView.panelView.children.add( toolbarView ); toolbarView.fillFromConfig( editor.config.get( 'formattingOptions' ), editor.ui.componentFactory ); return dropdownView; } ); } } DecoupledEditor .create( { root: { element: document.querySelector( '#editor-content' ), }, licenseKey: 'GPL', // Or ''. plugins: [ Alignment, Autoformat, BlockQuote, Bold, Essentials, Font, Heading, HorizontalLine, Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Indent, Italic, Link, List, MediaEmbed, Paragraph, RemoveFormat, Strikethrough, Subscript, Superscript, Table, TableToolbar, Underline, FormattingOptions ], toolbar: [ 'undo', 'redo', '|', 'formattingOptions', '|', 'link', 'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'horizontalLine', '|', { label: 'Lists', icon: false, items: [ 'bulletedList', 'numberedList', '|', 'outdent', 'indent' ] } ], // Configuration of the formatting dropdown. formattingOptions: [ 'undo', 'redo', '|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'alignment', '|', 'bulletedList', 'numberedList', '|', 'outdent', 'indent', '|', 'removeFormat' ], image: { resizeUnit: 'px', toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] } } ) .then( editor => { window.editor = editor; const toolbarContainer = document.querySelector( '#editor-toolbar-container' ); toolbarContainer.appendChild( editor.ui.view.toolbar.element ); overrideDropdownPositionsToNorth( editor, editor.ui.view.toolbar ); overrideDropdownPositionsToNorth( editor, editor.plugins.get( 'FormattingOptions' ).toolbarView ); overrideTooltipPositions( editor.ui.view.toolbar ); overrideTooltipPositions( editor.plugins.get( 'FormattingOptions' ).toolbarView ); } ) .catch( err => { console.error( err.stack ); } ); /** * Force all toolbar dropdown panels to use northern positions rather than southern (editor default). * This will position them correctly relative to the toolbar at the bottom of the editing root. * * @private * @param {module:core/editor/editor~Editor} editor * @param {module:ui/toolbar/toolbarview~ToolbarView} toolbarView */ function overrideDropdownPositionsToNorth( editor, toolbarView ) { const { south, north, southEast, southWest, northEast, northWest, southMiddleEast, southMiddleWest, northMiddleEast, northMiddleWest } = DropdownView.defaultPanelPositions; let panelPositions; if ( editor.locale.uiLanguageDirection !== 'rtl' ) { panelPositions = [ northEast, northWest, northMiddleEast, northMiddleWest, north, southEast, southWest, southMiddleEast, southMiddleWest, south ]; } else { panelPositions = [ northWest, northEast, northMiddleWest, northMiddleEast, north, southWest, southEast, southMiddleWest, southMiddleEast, south ]; } for ( const item of toolbarView.items ) { if ( !( item instanceof DropdownView ) ) { continue; } item.on( 'change:isOpen', () => { if ( !item.isOpen ) { return; } item.panelView.position = DropdownView._getOptimalPosition( { element: item.panelView.element, target: item.buttonView.element, fitInViewport: true, positions: panelPositions } ).name; } ); } } /** * Forces all toolbar items to display tooltips to the north. * This will position them correctly relative to the toolbar at the bottom of the editing root. * * @param {module:ui/toolbar/toolbarview~ToolbarView} toolbarView */ function overrideTooltipPositions( toolbarView ) { for ( const item of toolbarView.items ) { if ( item.buttonView ) { item.buttonView.tooltipPosition = 'n'; } else if ( item.tooltipPosition ) { item.tooltipPosition = 'n'; } } } ``` View editor content listing ```
Editor content is inserted here.
```
source file: "ckeditor5/latest/examples/framework/chat-with-mentions.html" ## Chat with mentions The [mention](#ckeditor5/latest/features/mentions.html) feature allows developing rich–text applications (like chats) with autocomplete suggestions displayed in a dedicated panel as the user types a pre-configured marker. For instance, in the editor below, type **“@”** to mention users and **“#”** to select from the list of available tags. Learn how to [configure mention feeds](#ckeditor5/latest/features/mentions.html--configuration) in the dedicated guide and check out the full source code of this example below if you want to implement your own chat using CKEditor 5 WYSIWYG editor. ### Editor example configuration The following code will let you run the editor inside a chat application like in the example above. View editor configuration script ``` import { ClassicEditor, Bold, Italic, Strikethrough, Underline, Essentials, Link, Mention, Paragraph } from 'ckeditor5'; ClassicEditor .create( { attachTo: document.querySelector( '.chat__editor' ), licenseKey: 'GPL', // Or ''. extraPlugins: [ Essentials, Paragraph, Mention, MentionLinks, Bold, Italic, Underline, Strikethrough, Link ], toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, mention: { feeds: [ { marker: '@', feed: [ { id: '@cflores', avatar: 'm_1', name: 'Charles Flores' }, { id: '@gjackson', avatar: 'm_2', name: 'Gerald Jackson' }, { id: '@wreed', avatar: 'm_3', name: 'Wayne Reed' }, { id: '@lgarcia', avatar: 'm_4', name: 'Louis Garcia' }, { id: '@rwilson', avatar: 'm_5', name: 'Roy Wilson' }, { id: '@mnelson', avatar: 'm_6', name: 'Matthew Nelson' }, { id: '@rwilliams', avatar: 'm_7', name: 'Randy Williams' }, { id: '@ajohnson', avatar: 'm_8', name: 'Albert Johnson' }, { id: '@sroberts', avatar: 'm_9', name: 'Steve Roberts' }, { id: '@kevans', avatar: 'm_10', name: 'Kevin Evans' }, { id: '@mwilson', avatar: 'w_1', name: 'Mildred Wilson' }, { id: '@mnelson', avatar: 'w_2', name: 'Melissa Nelson' }, { id: '@kallen', avatar: 'w_3', name: 'Kathleen Allen' }, { id: '@myoung', avatar: 'w_4', name: 'Mary Young' }, { id: '@arogers', avatar: 'w_5', name: 'Ashley Rogers' }, { id: '@dgriffin', avatar: 'w_6', name: 'Debra Griffin' }, { id: '@dwilliams', avatar: 'w_7', name: 'Denise Williams' }, { id: '@ajames', avatar: 'w_8', name: 'Amy James' }, { id: '@randerson', avatar: 'w_9', name: 'Ruby Anderson' }, { id: '@wlee', avatar: 'w_10', name: 'Wanda Lee' } ], itemRenderer: customItemRenderer }, { marker: '#', feed: [ '#american', '#asian', '#baking', '#breakfast', '#cake', '#caribbean', '#chinese', '#chocolate', '#cooking', '#dairy', '#delicious', '#delish', '#dessert', '#desserts', '#dinner', '#eat', '#eating', '#eggs', '#fish', '#food', '#foodgasm', '#foodie', '#foodporn', '#foods', '#french', '#fresh', '#fusion', '#glutenfree', '#greek', '#grilling', '#halal', '#homemade', '#hot', '#hungry', '#icecream', '#indian', '#italian', '#japanese', '#keto', '#korean', '#lactosefree', '#lunch', '#meat', '#mediterranean', '#mexican', '#moroccan', '#nom', '#nomnom', '#paleo', '#poultry', '#snack', '#spanish', '#sugarfree', '#sweet', '#sweettooth', '#tasty', '#thai', '#vegan', '#vegetarian', '#vietnamese', '#yum', '#yummy' ] } ] } } ) .then( editor => { const editingView = editor.editing.view; const rootElement = editingView.document.getRoot(); window.editor = editor; // Clone the first message in the chat when "Send" is clicked, fill it with new data // and append to the chat list. document.querySelector( '.chat-send' ).addEventListener( 'click', () => { const message = editor.getData(); if ( !message ) { editingView.change( writer => { writer.addClass( 'highlighted', rootElement ); editingView.focus(); } ); setTimeout( () => { editingView.change( writer => { writer.removeClass( 'highlighted', rootElement ); } ); }, 650 ); return; } const clone = document.querySelector( '.chat__posts li' ).cloneNode( true ); clone.classList.add( 'new-post' ); clone.querySelector( 'img' ).src = 'https://ckeditor.com/docs/ckeditor5/latest/assets/img/m_0.jpg'; clone.querySelector( 'strong' ).textContent = 'CKEditor User'; const mailtoUser = clone.querySelector( '.chat__posts__post__mailto-user' ); mailtoUser.textContent = '@ckeditor'; mailtoUser.href = 'mailto:info@cksource.com'; clone.querySelector( '.chat__posts__post__time' ).textContent = 'just now'; clone.querySelector( '.chat__posts__post__content' ).innerHTML = message; document.querySelector( '.chat__posts' ).appendChild( clone ); editor.setData( '' ); editingView.focus(); } ); } ) .catch( err => { console.error( err.stack ); } ); /* * This plugin customizes the way mentions are handled in the editor model and data. * Instead of a classic , */ function MentionLinks( editor ) { // The upcast converter will convert a view // // ... // // element to the model "mention" text attribute. editor.conversion.for( 'upcast' ).elementToAttribute( { view: { name: 'a', key: 'data-mention', classes: 'mention', attributes: { href: true } }, model: { key: 'mention', value: viewItem => editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem ) }, converterPriority: 'high' } ); // Downcast the model "mention" text attribute to a view // // ... // // element. editor.conversion.for( 'downcast' ).attributeToElement( { model: 'mention', view: ( modelAttributeValue, { writer, options } ) => { // Do not convert empty attributes (lack of value means no mention). if ( !modelAttributeValue ) { return; } let href; // User mentions are downcasted as mailto: links. Tags become normal URLs. if ( modelAttributeValue.id[ 0 ] === '@' ) { href = `mailto:${ modelAttributeValue.id.slice( 1 ) }@example.com`; } else { href = `https://example.com/social/${ modelAttributeValue.id.slice( 1 ) }`; } return writer.createAttributeElement( 'a', { class: 'mention', 'data-mention': modelAttributeValue.id, href, // Omit `data-mention-uid` in clipboard (copy/cut) to prevent UIDs duplication. ...( !options.isClipboardPipeline && { 'data-mention-uid': modelAttributeValue.uid } ) }, { // Make mention attribute to be wrapped by other attribute elements. priority: 20, // Prevent merging mentions together in clipboard (when `data-mention-uid` is not available). id: modelAttributeValue.uid } ); }, converterPriority: 'high' } ); } /* * Customizes the way the list of user suggestions is displayed. * Each user has an @id, a name and an avatar. */ function customItemRenderer( item ) { const itemElement = document.createElement( 'span' ); const avatar = document.createElement( 'img' ); const userNameElement = document.createElement( 'span' ); const fullNameElement = document.createElement( 'span' ); itemElement.classList.add( 'mention__item' ); avatar.src = `https://ckeditor.com/docs/ckeditor5/latest/assets/img/${ item.avatar }.jpg`; userNameElement.classList.add( 'mention__item__user-name' ); userNameElement.textContent = item.id; fullNameElement.classList.add( 'mention__item__full-name' ); fullNameElement.textContent = item.name; itemElement.appendChild( avatar ); itemElement.appendChild( userNameElement ); itemElement.appendChild( fullNameElement ); return itemElement; } ``` View editor content listing ```

I agree with @mwilson 👍. It’s so nice of you to always be providing a few options to try! I love #greek cuisine with a modern twist, this one will be perfect to try.

``` source file: "ckeditor5/latest/examples/framework/custom-ui.html" ## Custom UI (with Bootstrap) The editor below runs a completely custom user interface written in [Bootstrap](http://getbootstrap.com/), while the editing is provided by CKEditor 5. ### Detailed guide If you would like to create this interface on your own, read the [dedicated tutorial](#ckeditor5/latest/framework/deep-dive/ui/external-ui.html) that shows how to achieve this step by step with the source code provided. source file: "ckeditor5/latest/examples/framework/theme-customization.html" ## Theme customization The default theme of CKEditor 5 can be customized to match most visual integration requirements. Below, you can see an editor with the dark theme as a result of customizations described in a [dedicated guide](#ckeditor5/latest/framework/deep-dive/ui/theme-customization.html). **Mode:** Light Dark ### Detailed guide If you would like to create such a widget on your own, read the [dedicated tutorial](#ckeditor5/latest/framework/deep-dive/ui/theme-customization.html) that shows how to achieve this step by step with the source code provided. source file: "ckeditor5/latest/examples/index.html" ## Examples Check out these examples of different editor integrations. See the various editor types in action, witness the unharnessed power of a feature-rich preset, and find out amazing, custom-tailored implementations using the CKEditor 5 Framework. ### Editor types CKEditor 5 offers several rich text editor types. They cover the most common editing use cases, and you can install them easily. See all of them in action: [classic](#ckeditor5/latest/examples/builds/classic-editor.html), [inline](#ckeditor5/latest/examples/builds/inline-editor.html), [balloon](#ckeditor5/latest/examples/builds/balloon-editor.html), [balloon block](#ckeditor5/latest/examples/builds/balloon-block-editor.html), [document](#ckeditor5/latest/examples/builds/document-editor.html), and [multi-root](#ckeditor5/latest/examples/builds/multi-root-editor.html) editors! Learn their differences by using our examples. ### Advanced presets CKEditor 5 is a configurable framework created with collaboration in mind. It lets you build a custom editor of any type, with a wide set of features and the toolbar type that you need where multiple authors can easily work on the same rich text documents. Check out our [feature-rich editor](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) that sports as many plugins as possible to put together. Want to invite some friends? Try the [collaborative document editor](#ckeditor5/latest/examples/builds-custom/collaborative-document-editor.html) that brings the experience of the most popular online editing solutions to the table. ### Advanced configuration – CKEditor 5 Framework examples [CKEditor 5 Framework](#ckeditor5/latest/framework/index.html) is a highly flexible and universal platform that provides a set of components allowing you to create any kind of rich text editor. It enables the integrators to build different, custom-tailored editing solutions with [custom UI](#ckeditor5/latest/examples/framework/custom-ui.html) or [a theme](#ckeditor5/latest/examples/framework/theme-customization.html) that suit their specific needs. It also provides tools for customizing [existing ones](#ckeditor5/latest/examples/framework/chat-with-mentions.html). And witness the flexibility of the UI in the [toolbar-oriented example](#ckeditor5/latest/examples/framework/bottom-toolbar-editor.html). source file: "ckeditor5/latest/features/accessibility.html" ## Accessibility support CKEditor 5 incorporates various accessibility features, including keyboard navigation, screen reader support (ARIA attributes), and robust semantic output markup. This guide provides a detailed overview and presents the current status of editor accessibility. ### Conformance with WCAG 2.x and Section 508 CKEditor 5 is compliant with [Web Content Accessibility Guidelines 2.2](https://www.w3.org/TR/WCAG22/) (WCAG) 2.2 levels A and AA and [Section 508 of the Rehabilitation Act](https://www.access-board.gov/ict/) unless stated otherwise in the [Accessibility Conformance Report](#ckeditor5/latest/features/accessibility.html--accessibility-conformance-report-vpat). * [Web Content Accessibility Guidelines 2.2](https://www.w3.org/TR/WCAG22/) (WCAG) provides international standards for making web content accessible to individuals with disabilities, ensuring that web applications are perceivable, operable, understandable, and robust for all users. * [Section 508 of the Rehabilitation Act](https://www.access-board.gov/ict/) mandates that federal agencies’ electronic and information technology is accessible to people with disabilities, establishing guidelines to achieve this goal. CKEditor 5 strives for conformance with these standards and we welcome your [feedback](#ckeditor5/latest/features/accessibility.html--accessibility-feedback-and-bugs) on the accessibility of our software. ### Recommended software For optimal screen reader experience, we recommend using Google Chrome and NVDA (Windows) or Safari and VoiceOver (macOS). ### Accessibility Conformance Report (VPAT) In our ongoing commitment to accessibility, we provide a report based on the [ITI Voluntary Product Accessibility Template](https://www.itic.org/policy/accessibility/vpat) (VPAT\*\*®\*\*), a standardized format for evaluating the accessibility of computer software. This document serves as a comprehensive resource detailing the accessibility features of CKEditor 5, including compliance with accessibility standards and guidelines: [Web Content Accessibility Guidelines 2.2](https://www.w3.org/TR/WCAG22/) (WCAG) 2.2 levels A and AA and [Section 508 of the Rehabilitation Act](https://www.access-board.gov/ict/). We continuously update the VPAT**®** report to reflect any changes or improvements. You can download the latest version of the VPAT**®** document below. 📎 [**Download VPAT® report for CKEditor 5 v44.3.0 (Mar 5, 2025)**](../assets/pdf/VPAT%5FCKEditor%5F5%5Fv44.3.0.pdf) #### Previous versions * 📎 [VPAT® report for CKEditor 5 v43.0.0 (Aug 7, 2024)](../assets/pdf/VPAT%5FCKEditor%5F5%5Fv43.0.0.pdf) * 📎 [VPAT® report for CKEditor 5 v41.4.2 (May 17, 2024)](../assets/pdf/VPAT%5FCKEditor%5F5%5Fv41.4.2.pdf) * 📎 [VPAT® report for CKEditor 5 v41.3.0 (Apr 10, 2024)](../assets/pdf/VPAT%5FCKEditor%5F5%5Fv41.3.0.pdf) ### Keyboard shortcuts CKEditor 5 supports various keyboard shortcuts that boost productivity and provide necessary accessibility to screen reader users. Below is a list of the most important keystrokes supported by CKEditor 5 and its features. #### Content editing keystrokes These keyboard shortcuts allow for quick access to content editing features. | Action | PC | Mac | | ------------------------------------- | -------------------- | -------------------- | | Insert a hard break (a new paragraph) | Enter | | | Insert a soft break (a
element) | Shift+Enter | ⇧Enter | | Copy selected content | Ctrl+C | ⌘C | | Paste content | Ctrl+V | ⌘V | | Paste content as plain text | Ctrl+Shift+V | ⌘⇧V | | Undo | Ctrl+Z | ⌘Z | | Redo | Ctrl+Y, Ctrl+Shift+Z | ⌘Y, ⌘⇧Z | | Bold text | Ctrl+B | ⌘B | | Change text case | Shift+F3 | ⇧F3 (may require Fn) | | Create link | Ctrl+K | ⌘K | | Move out of a link | ←←, →→ | | | Move out of an inline code style | ←←, →→ | | | Select all | Ctrl+A | ⌘A | | Find in the document | Ctrl+F | ⌘F | | Copy text formatting | Ctrl+Shift+C | ⌘⇧C | | Paste text formatting | Ctrl+Shift+V | ⌘⇧V | | Italic text | Ctrl+I | ⌘I | | Strikethrough text | Ctrl+Shift+X | ⌘⇧X | | Underline text | Ctrl+U | ⌘U | | Revert autoformatting action | Backspace | |
##### Keystrokes for interacting with annotation threads (such as comments or track changes suggestions) | Action | PC | Mac | | ---------------------------------------------------------------------------------- | ------------ | ------ | | Move focus to the thread when the selection is anchored in its marker | Ctrl+Shift+E | ⌘⇧E | | Exit the annotation and move focus back to the edited content | Esc | | | Browse the focused annotation thread or thread comment | Enter | | | Move across internals of the annotation thread | ⇥, Shift+⇥ | ⇥, ⇧⇥ | | Submit the reply while writing a comment | Ctrl+Enter | ⌘Enter | | Move to the previous or next thread in the annotations sidebar or comments archive | ↑, ↓ | | ##### Keystrokes that can be used when a widget is selected (such as image, table, etc.) | Action | PC | Mac | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------ | | Insert a new paragraph directly after a widget | Enter | | | Insert a new paragraph directly before a widget | Shift+Enter | ⇧Enter | | Move the caret to allow typing directly before a widget | ↑, ← | | | Move the caret to allow typing directly after a widget | ↓, → | | | After entering a nested editable, move the selection to the closest ancestor widget. For example: move from an image caption to the whole image widget. | Tab then Esc | | ##### Keystrokes that can be used in a list | Action | PC | Mac | | ------------------------- | ------- | --- | | Increase list item indent | ⇥ | | | Decrease list item indent | Shift+⇥ | ⇧⇥ | ##### Keystrokes for navigating through documents | Action | PC | Mac | | --------------------------------------------- | --------------- | --------------------------- | | Go to the previous page (also move selection) | Shift+Page Up | ⇧Page Up (may require Fn) | | Go to the next page (also move selection) | Shift+Page Down | ⇧Page Down (may require Fn) | ##### Keystrokes that can be used in a table cell | Action | PC | Mac | | --------------------------------------------------------- | ---------- | --- | | Move the selection to the next cell | ⇥ | | | Move the selection to the previous cell | Shift+⇥ | ⇧⇥ | | Insert a new table row (when in the last cell of a table) | ⇥ | | | Navigate through the table | ↑, →, ↓, ← | | #### User interface and content navigation keystrokes Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface. | Action | PC | Mac | | ---------------------------------------------------------------------------------------------------------------------------------- | -------------- | --------------------- | | Close contextual balloons, dropdowns, and dialogs | Esc | | | Open the accessibility help dialog | Alt+0 | ⌥0 | | Move focus between form fields (inputs, buttons, etc.) | ⇥, Shift+⇥ | ⇥, ⇧⇥ | | Move focus to the toolbar, navigate between toolbars | Alt+F10 | ⌥F10 (may require Fn) | | Navigate through the toolbar or menu bar | ↑, →, ↓, ← | | | Navigate to the next focusable field or an element outside the editor | Tab, Shift+Tab | | | Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content. | Enter, Space | | | Move focus to the menu bar, navigate between menu bars | Alt+F9 | ⌥F9 (may require Fn) | | Move focus in and out of an active dialog window | Ctrl+F6 | ⌘F6 (may require Fn) | #### Source Editing Enhanced (plugin) Keystrokes introduced by [Source Editing Enhanced plugin](#ckeditor5/latest/features/source-editing/source-editing-enhanced.html), to streamline code editing experience. ##### Keystrokes related to the built-in code completion mechanism | Action | PC | Mac | | ---------------------------------------------- | --------- | --- | | Accept completion | Enter | | | Close completion | Esc | | | Move completion selection backward | ↑ | | | Move completion selection backward by one page | Page Up | | | Move completion selection forward | ↓ | | | Move completion selection forward by one page | Page Down | | ##### Keystrokes related to the built-in code folding mechanism | Action | PC | Mac | | ----------- | ------------- | ---- | | Fold all | Ctrl+Alt+\[ | ⌃⌥\[ | | Fold code | Ctrl+Shift+\[ | ⌘⌥\[ | | Unfold all | Ctrl+Alt+\] | ⌃⌥\] | | Unfold code | Ctrl+Shift+\] | ⌘⌥\] | ##### Keystrokes that change the selection in the code editor | Action | PC | Mac | | ----------------------------- | --------------- | --------------- | | Select all | Ctrl+A | ⌘A | | Select character left | Shift+← | ⇧←, ⌃⇧B | | Select character right | Shift+→ | ⇧→, ⌃⇧F | | Select document end | Shift+Ctrl+End | ⌘⇧↓, ⌘⇧End | | Select document start | Shift+Ctrl+Home | ⌘⇧↑, ⌘⇧Home | | Select group left | Shift+Ctrl← | ⌥⇧← | | Select group right | Shift+Ctrl→ | ⌥⇧→ | | Select line | Alt+L | ⌃L | | Select line boundary backward | Shift+Home | ⇧Home | | Select line boundary forward | Shift+End | ⇧End | | Select line down | Shift+↓ | ⇧↓, ⌃⇧N | | Select line up | Shift+↑ | ⇧↑, ⌃⇧P | | Select page down | Shift+Page Down | ⌃⇧↓, ⇧Page Down | | Select page up | Shift+Page Up | ⌃⇧↑, ⇧Page Up | | Select parent syntax | Ctrl+I | ⌘I | | Select syntax left | Shift+Alt+← | ⌃⇧← | | Select syntax right | Shift+Alt+→ | ⌃⇧→ | | Select line boundary left | | ⌘⇧← | | Select line boundary right | | ⌘⇧→ | | Select line end | | ⌃⇧E | | Select line start | | ⌃⇧A | ##### Keystrokes that move the cursor (caret) in the code editor | Action | PC | Mac | | ------------------------------------- | ------------ | ----------------- | | Move cursor to character left | ← | ←, ⌃B | | Move cursor to character right | → | →, ⌃F | | Move cursor to document end | Ctrl+End | ⌘↓, ⌘End | | Move cursor to document start | Ctrl+Home | ⌘↑, ⌘Home | | Move cursor to group left | Ctrl+← | ⌥← | | Move cursor to group right | Ctrl+→ | ⌥→ | | Move cursor to line boundary backward | Home | | | Move cursor to line boundary forward | End | | | Move cursor to line down | ↓ | ↓, ⌃N | | Move cursor to line up | ↑ | ↑, ⌃P | | Move cursor to matching bracket | Shift+Ctrl\\ | ⌘⇧\\ | | Move cursor to page down | Page Down | ⌃↓, ⌃V, Page Down | | Move cursor to page up | Page Up | ⌃↑, Page Up | | Move cursor to syntax left | Alt+← | ⌃← | | Move cursor to syntax right | Alt+→ | ⌃→ | | Move line down | Alt+↓ | ⌥↓ | | Move line up | Alt+↑ | ⌥↑ | | Move cursor to line boundary left | | ⌘← | | Move cursor to line boundary right | | ⌘→ | | Move cursor to line end | | ⌘E | | Move cursor to line start | | ⌘A | ##### Keystrokes that modify the code in the editor | Action | PC | Mac | | ----------------------------- | -------------------------- | ------------------------- | | Copy line down | Shift+Alt+↓ | ⇧⌥↓ | | Copy line up | Shift+Alt+↑ | ⇧⌥↑ | | Delete bracket pair | Backspace | | | Delete character backward | Backspace, Shift+Backspace | Backspace, ⌃H, ⇧Backspace | | Delete character forward | Delete | ⌃D, Delete | | Delete group backward | Ctrl+Backspace | ⌥Backspace, ⌃⌥H | | Delete group forward | Ctrl+Delete | ⌥Delete | | Delete line | Shift+CtrlK | ⌘⇧K | | Indent less | Ctrl+\[ | ⌘\[ | | Indent more | Ctrl+\] | ⌘\] | | Indent selection | Ctrl+Alt+\\ | ⌘⌥\\ | | Insert blank line | Ctrl+Enter | ⌘Enter | | Insert new line and indent | Enter, Shift+Enter | Enter, ⇧Enter | | Redo | Ctrl+Shift+Z, Ctrl+Y | ⌘⇧Z, ⌘Y | | Redo selection | Alt+U | ⌘⇧U | | Simplify selection | Esc | | | Toggle block comment | Shift+Alt+A | ⇧⌥A | | Toggle comment | Ctrl+/ | ⌘/ | | Undo | Ctrl+Z | ⌘Z | | Undo selection | Ctrl+U | ⌘U | | Delete line boundary backward | | ⌘Backspace | | Delete line boundary forward | | ⌘Delete | | Delete to line end | | ⌃K | | Split line | | ⌃O | | Transpose characters | | ⌃T | ##### Miscellaneous code editor shortcuts | Action | PC | Mac | | ---------------------------------------------------------- | ------ | --- | | Switch between "focus with tab" and "indent with tab" mode | Ctrl+M | ⌥⇧M | #### Displaying keyboard shortcuts in the editor CKEditor 5 offers a dedicated [Accessibility help](../api/module%5Fui%5Feditorui%5Faccessibilityhelp%5Faccessibilityhelp-AccessibilityHelp.html) plugin that displays a list of all available keyboard shortcuts in a dialog. It can be opened by pressing Alt \+ 0 (on Windows) or ⌥0 (on macOS). Alternatively, you can use the toolbar button to open the dialog. The Accessibility help plugin is enabled by the [Essentials](../api/module%5Fessentials%5Fessentials-Essentials.html) plugin from the [@ckeditor/ckeditor5-essentials](https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/api/essentials.html) package (which also enables other common editing features). Learn how integrators can [add keyboard shortcuts to their features](#ckeditor5/latest/framework/tutorials/crash-course/keystrokes.html--adding-keyboard-shortcuts) and [supply shortcut information](#ckeditor5/latest/framework/tutorials/crash-course/keystrokes.html--adding-shortcut-information-to-the-accessibility-help-dialog) to the Accessibility help dialog. ### Accessibility feedback and bugs We welcome your feedback on the accessibility of CKEditor 5\. You can find the [current list of accessibility issues](https://github.com/ckeditor/ckeditor5/issues?q=is%3Aopen+is%3Aissue+label%3Adomain%3Aaccessibility) on GitHub. Learn how to [report issues](#ckeditor5/latest/support/index.html--reporting-issues). source file: "ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html" ## Integrating AI Assistant with your application This guide will take you step-by-step through the process of running AI Assistant in your editor integration. It also presents possible configuration and customization options. ### Supported AI services First, you will need to decide which AI service provider you want to integrate with. CKEditor does not provide the AI model itself. Instead, the feature relies on an external service to provide AI-generated responses. We offer support for two leading platforms that provide AI services: **OpenAI and Azure OpenAI.** Since the feature relies on an external provider, the quality of the responses depends on that provider and their model. If you have no constraints regarding the platform that you can use, **we recommend integrating with the OpenAI API**. It provides better quality and is the simplest to set up. This guide includes tips on how to set up the supported AI platforms. We expect that the integrator knows how their chosen platform works and how to configure it to best fit their use case. ### Using proxy endpoint Before moving to the integration, there is one more subject to cover. There are two general approaches to how the feature can communicate with the AI service provider: directly, or using an endpoint in your application. Direct connection is simpler to set up and should not involve changes in your application’s backend. It is recommended for development purposes. AI Assistant supports this, as it makes it easier for you to test the feature without committing time to set up the backend part of the integration. **However, this method exposes your private authorization data which is a serious security issue. You should never use it in the production environment.** In the final solution, your application should provide an endpoint that the AI Assistant will call instead of calling the AI service directly. The main goal of this endpoint is to hide the authorization data from the editor users. The request to the AI service should happen from your backend, without exposing authorization credentials. The application endpoint is also a good place to implement additional functionalities, like request customization, user billing, or logging statistics. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Integration In the next step, you will need to set up the AI service of your choice and integrate the editor to use it: * [OpenAI integration](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--openai-integration) * [Azure OpenAI integration](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--azure-openai-integration) #### OpenAI integration This section describes how to integrate the AI Assistant with the [OpenAI platform](https://openai.com/). ##### Set up the account [Create](https://platform.openai.com/login?launch) an OpenAI account and get your [OpenAI API](https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key) key. ##### Making connection To connect to the OpenAI service, you will need to add a connection adapter plugin to the editor. The adapter is responsible for making requests in the correct format and handling the responses. Import the [OpenAITextAdapter](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapter.html) plugin from the `ckeditor5-ai` package and add it to the list of plugins. Then, add the OpenAI key to the editor configuration. You should send the key in the request “Authorization” header. You can set the request headers using the [config.ai.assistant.adapter.openAI.requestHeaders](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapterConfig.html#member-requestHeaders) configuration property. The snippet below presents the described changes: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { assistant: { adapter: { openAI: { requestHeaders: { // Paste your OpenAI API key in place of YOUR_OPENAI_API_KEY: Authorization: 'Bearer YOUR_OPENAI_API_KEY' } } } // ... } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` This is the minimal setup required to launch AI Assistant. **You can test it now.** ##### Request parameters You can further configure how the OpenAI adapter works using the [config.ai.assistant.adapter.openAI.requestParameters](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapterConfig.html#member-requestParameters) option: * Choose the exact OpenAI model to use. * Set whether the response should be streamed (simulating the “writing” experience) or returned all at once. * Fine-tune the model behavior. See the [OpenAI reference](https://platform.openai.com/docs/api-reference/chat/create) to learn what parameters you can use and how they affect the responses. ##### Supported models By default, the OpenAI adapter will use the GPT-4o model. CKEditor 5 supports all recent GPT-3.5 and GPT-4 models as well as legacy models (version `0613`). You can find more information about offered models in the [OpenAI documentation](https://platform.openai.com/docs/models/). ##### Integrating with the proxy endpoint As [described earlier](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--using-proxy-endpoint), before moving to production, you should create an endpoint that will communicate with the OpenAI service, instead of connecting directly and exposing your OpenAI API key. For the OpenAI integration, you can implement the endpoint, in its simplest form, as a transparent proxy service. The service will get the requests from the editor, add authorization headers to them, and pass them to the AI service. Then, you should pass all responses back to the editor. After you implemented the endpoint, set the URL to your endpoint using the [config.ai.assistant.adapter.openAI.apiUrl](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapterConfig.html#member-apiUrl) option. Also, remember to remove the OpenAI key from the configuration: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { assistant: { adapter: { openAI: { apiUrl: 'https://url.to.your.application/ai' } } // ... } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Now, all requests are redirected to `'https://url.to.your.application/ai'`. ##### Additional authorization and custom headers Depending on your application, it might be necessary to pre-authorize the request before sending it to your application endpoint. One of the common patterns is to use JSON Web Token (JWT) authorization. This, and similar cases, are supported through the [config.ai.assistant.adapter.openAI.requestHeaders](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapterConfig.html#member-requestHeaders) option. You can set it to an object or an asynchronous function that resolves with an object. The object is then set as the request headers. You can set `config.ai.assistant.adapter.openAI.requestHeaders` to a function that queries the authorization API and sets the returned JWT in an authorization header: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { assistant: { adapter: { openAI: { apiUrl: 'https://url.to.your.application/ai', requestHeaders: async () => { const jwt = await fetch( 'https://url.to.your.auth.endpoint/' ); return { Authorization: 'Bearer ' + jwt }; } } } // ... } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` You can pass the [actionId](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html#member-actionId) parameter to the `requestHeaders` function. It identifies the action that the user performed. This allows for further customization on your end. ``` { requestHeaders: async ( actionId ) => { const jwt = await fetch( 'https://url.to.your.auth.endpoint/?actionId=' + actionId ); return { Authorization: 'Bearer ' + jwt }; } } ``` ##### Advanced customization The most flexible place to apply request processing customization is your application endpoint. However, if for any reason you cannot customize the request on your application’s backend, you can consider the following extension points on the editor side. **Dynamic request headers.** As mentioned earlier, you can provide `config.ai.assistant.adapter.openAI.requestHeaders` as an asynchronous function that can make a call to your application. You can use the [actionId](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html#member-actionId) parameter for further customization. **Dynamic request parameters.** Similarly to request headers, you can provide `config.ai.assistant.adapter.openAI.requestParameters` as an asynchronous function. The function is also passed the [actionId](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html#member-actionId). You can return different parameters based on it. For example, you can use different models for different actions. **Customizing [request messages](https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages).** The request messages passed to the OpenAI service are built based on how the user used the feature: the selected content and the provided query. You can overload the [OpenAITextAdapter#prepareMessages()](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapter.html#function-prepareMessages) method to customize the request messages or provide custom logic that will create the request messages. For example: * You can fine-tune the system message for specific (or your custom) predefined commands. * You can pre-query your application to get extra context information and add it as an additional message. * You can get additional context or data from the editor, document data, or your custom features. * You can alter or redact parts of the `context` before sending it to the service. Remember to add `CustomOpenAITextAdapter` to the plugin list instead of `OpenAITextAdapter`. **Altering AI service responses** Each feature that sends a request provides the [onData()](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html#member-onData) callback. The callback is executed each time the adapter receives the data from the AI service. You can decorate this callback to customize the response. This will require overloading the [OpenAITextAdapter#sendRequest()](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapter.html#function-sendRequest) method and changing the `requestData.onData` parameter: If the adapter works in the streaming mode, the `content` will include a partial, accumulating response. This may bring some extra complexity to your custom handling. Remember to add `CustomOpenAITextAdapter` to the plugin list instead of `OpenAITextAdapter`. **Overloading the [sendRequest()](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapter.html#function-sendRequest) method.** You can overload the `sendRequest()` method to add some processing before or after making the call. Remember to add `CustomOpenAITextAdapter` to the plugin list instead of `OpenAITextAdapter`. #### Azure OpenAI integration This section describes how to integrate the AI Assistant with the [Azure OpenAI Service](https://azure.microsoft.com/en-us/products/ai-services/openai-service). Microsoft’s Azure platform provides many AI-related services. AI Assistant supports only the OpenAI models. ##### Set up the service First, you will need to create an [Azure](https://azure.microsoft.com/) account if you do not already own one. You need to follow these steps to set up the AI Assistant: * Log in to your Azure account. * Create an “Azure OpenAI” resource. * Go to the “Azure OpenAI” resource and open “Keys and Endpoint” to find your API key(s). * Go to “Model deployments” and then create a deployment. Select the model and the name you want to use for that deployment. * You will need the resource name, API key, and deployment name to configure the AI Assistant. ##### Making connection To connect to the Azure OpenAI service, you will need to add a connection adapter plugin to the editor. The adapter is responsible for making requests in the correct format and handling the responses. Import the [OpenAITextAdapter](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapter.html) plugin from the `ckeditor5-ai` package and add it to the list of plugins. Then, you will need to configure the AI Assistant, so it connects to the Azure OpenAI service using your data: * The request URL as specified in the [Azure OpenAI reference](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions) (it will include your deployment name and the API version). * We tested AI Assistant with the `2023-12-01-preview` API version. * You need to pass the API key in the request `api-key` header. The snippet below presents the described changes: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { assistant: { adapter: { openAI: { // Paste your resource name, deployment name, and API version // in place of YOUR_RESORCE_NAME, YOUR_DEPLOYMENT_NAME, and YOUR_API_VERSION: apiUrl: 'https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=YOUR_API_VERSION', requestHeaders: { 'api-key': 'YOUR_AZURE_OPEN_AI_API_KEY' } } } } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` This is the minimal setup required to launch AI Assistant. **You can test it now.** ##### Request parameters You can further configure how the OpenAI adapter works using the [config.ai.assistant.adapter.openAI.requestParameters](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Fopenaitextadapter-OpenAITextAdapterConfig.html#member-requestParameters) option: * Set whether the response should be streamed (simulating the “writing” experience) or returned all at once. * Fine-tune the model behavior. See the [Azure OpenAI reference](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions) to learn what parameters you can use and how they affect the responses. You may also set `requestParameters` to an asynchronous function. In this case, it should resolve with an object that contains the parameters. The function receives [actionId](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html#member-actionId) as a parameter, which identifies the action that the user performed. This allows for further customization on your end. ##### Supported models and API versions CKEditor 5 supports all recent GPT-3.5 and GPT-4 models as well as legacy models (version `0613`). You can find more information about offered models in the [Azure OpenAI documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). The most recent tested API version is `2023-12-01-preview`. ##### Integrating with proxy endpoint As [described earlier](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--using-proxy-endpoint), before moving to production, you should create an endpoint that will communicate with the OpenAI service, instead of connecting directly and exposing your OpenAI API key. See the [“Integration with the proxy endpoint” section for the OpenAI integration](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--integrating-with-the-proxy-endpoint), as the process is the same for both platforms. ##### Advanced customization The most flexible place to apply request processing customization is your application endpoint. However, if for any reason you cannot customize the request on your application’s backend, you can extend the `OpenAITextAdapter`. There are many extension points which you may consider. See the [“Advanced customization” section for the OpenAI integration](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--advanced-customization), as it is the same for both platforms. #### Amazon Bedrock integration #### Custom models You can integrate AI Assistant with any service of your choice as well as your custom models. ##### Use OpenAI adapter and adjust AI service responses A simple way to provide support for a different AI model or service is to use the [OpenAI integration](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--openai-integration), and then [provide an endpoint](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--integrating-with-the-proxy-endpoint) in your application that will query the chosen model or service. You need to make sure that the responses passed to the adapter are in the same format as the OpenAI API responses. In the end, the adapter remains indifferent to what endpoint you connect to. Its role is to create the request data and handle the response. As long as the response format is the same as the one used by the OpenAI API, it will work. ##### Implement custom adapter Another method to support different models is to provide a custom implementation of the [AITextAdapter](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapter.html) plugin. This will give you more flexibility in creating the request and processing the response. The full implementation will depend on the requirements set by the chosen AI model. Start with defining your custom adapter class: From the editor’s perspective, you will need to implement the [sendRequest()](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapter.html#function-sendRequest) method: This is the place where you should handle the request. The [requestData](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faitextadapter-AITextAdapterRequestData.html) parameter includes the data provided by the feature (for example, AI Assistant) as the feature made the call to the adapter. The API documentation describes each part of `requestData`. To better understand it, here is a breakdown using the AI Assistant as an example: * `query` – The predefined command query or custom query provided by the user. The instruction for the AI model. * `context` – The HTML content selected in the editor when the user made the request. It may be empty. * `actionId` – For AI Assistant this could be `aiAssistant:custom` or `aiAssistant:command:`. You can use it to handle various user actions differently. * `onData` – For AI Assistant, it updates the UI (response area) and saves the updated response in the feature’s internals. You should call it each time the adapter gets an update from the AI service. In short, you should use `query` and `context` (and optionally `actionId`) to build a prompt for the AI service. Then call `onData()` when you receive a response. It could happen once (no streaming) or many times (streaming). Support for streaming depends on the AI service. Alternatively, you can pass `query`, `context`, and `actionId` in the request to your application endpoint and handle them on the backend. When your adapter fails for some reason, you should throw [AIRequestError](../../../api/module%5Fai%5Faiassistant%5Fadapters%5Faiadapter-AIRequestError.html). The error will be handled by the feature. In the case of AI Assistant, it will be displayed in a red notification box. Finally, add `CustomAITextAdapter` to the editor plugin list. Note, that you do not need to add any other adapter: ``` ClassicEditor .create( { plugins: [ AIAssistant, CustomAITextAdapter, /* ... */ ], /* .. */ } ) .then( /* ... */ ) .catch( /* ... */ ); ``` If the custom AI model supports streaming, you will receive the response in multiple small chunks. Make sure that each time the `onData()` callback is called, the value passed to it contains the full response. It needs to be a sum of the current update and all previously received responses. ### Configuration and styling #### Adding AI commands to the list The **“AI Commands”** button allows quick access to the most common AI Assistant commands. You can extend the [default list of commands](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-commands) or define your list. Use the [config.ai.assistant.extraCommandGroups](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-extraCommandGroups) configuration option to extend the default list of commands: ``` ClassicEditor .create( { ai: { // AI Assistant feature configuration. assistant: { // Extend the default commands configuration. extraCommandGroups: [ // Add a command to an existing group: { groupId: 'translate', commands: [ { id: 'translatePolish', label: 'Translate to Polish', prompt: 'Translate to Polish language.' } ] }, // Create a new AI commands group: { groupId: 'transformations', groupLabel: 'Transformations', commands: [ { id: 'addEmojis', label: 'Add emojis', prompt: 'Analyze each sentence of this text. After each sentence add an emoji that summarizes the sentence.' }, // ... ] }, ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Use the [config.ai.assistant.commands](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-commands) configuration option to create the list of commands from scratch: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { // AI Assistant feature configuration. assistant: { // Define the commands list from scratch. commands: [ // Command groups keep them organized on the list. { groupId: 'customGroupId', groupLabel: 'My group of commands', commands: [ { id: 'translateSpanish', label: 'Translate to Spanish', prompt: 'Translate this text to Spanish.' }, { id: 'explainFive', label: 'Explain like I\'m five', prompt: 'Explain this like I\'m five years old.' }, // ... ] }, // You can add more command groups here. ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Please note that you can avoid creating command groups by passing commands definitions directly to the `ai.assistant.commands` configuration key. This will result in a flat list in the user interface. #### Removing default commands from the list You can use the [config.ai.assistant.removeCommands](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-removeCommands) configuration to remove some [default commands and command groups](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-commands) from the list: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { // AI Assistant feature configuration. assistant: { // Remove some of the default commands. removeCommands: [ // Use command id to remove a single command. 'improveWriting', // Use groupId to remove entire command group. 'changeTone', // ... ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Removing the violet tint from the UI By default, some parts of the UI come with a violet tint that distinguishes the AI Assistant from the rest of CKEditor 5 features. If you do not want this styling in your integration, you can remove it by setting the [config.ai.assistant.useTheme](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html) configuration to `false`: ``` ClassicEditor .create( { // ... Other configuration options ... ai: { assistant: { // Remove the default feature's theme. useTheme: false } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Using custom colors for the UI You can customize the looks of the AI Assistant UI by using CSS custom properties. Below is the full list of CSS variables that you can set. For instance, you can use the following CSS snippet to change the tint color to red: ``` .ck-ai-assistant-ui_theme { --ck-color-button-default-hover-background: hsl(0, 100%, 96%); --ck-color-button-default-active-background: hsl(0,100%,96.3%); --ck-color-button-on-background: hsl(0,100%,96.3%); --ck-color-button-on-hover-background: hsl(0,60%,92.2%); --ck-color-button-on-active-background: hsl(0,100%,96.3%); --ck-color-button-on-disabled-background: hsl(0,100%,96.3%); --ck-color-button-on-color: hsl(0,59.2%,52%); --ck-color-button-action-background: hsl(0,59.2%,52%); --ck-color-button-action-hover-background: hsl(0,58.9%,49.6%); --ck-color-button-action-active-background: hsl(0,58.9%,49.6%); --ck-color-button-action-disabled-background: hsl(0,59.3%,75.9%); --ck-color-list-button-hover-background: hsl(0,100%,96.3%); --ck-color-ai-selection: hsl(0,60%,90%); } ``` #### Changing the width of the dialog Use the following CSS snippet to widen the AI Assistant pop-up dialog: ``` .ck.ck-ai-form { --ck-ai-form-view-width: 800px; } ``` #### Changing the height of the response area Use the following CSS snippet to increase the `max-height` CSS property of the response content area and display more content to the users: ``` .ck.ck-ai-form { --ck-ai-form-content-height: 500px; } ``` #### Styling the AI response area By default, the AI Assistant’s response content area comes with the `.ck-content` CSS class. This makes it possible for the users to see the response styled in the same way as the main editor content (learn more about it in the [Content styles](#ckeditor5/latest/getting-started/setup/css.html) guide). However, if your integration uses custom styles outside the `.ck-content` class scope, and you want to apply them in the Assistant’s response content area, you can use [config.ai.assistant.contentAreaCssClass](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-contentAreaCssClass) and specify an additional class name (or names) for the element. Styling the AI Assistant’s response content area is also possible via the `.ck.ck-ai-form .ck.ck-ai-form__content-field` selector: ``` .ck.ck-ai-form .ck.ck-ai-form__content-field h2 { /* Custom

styles. */ } ``` source file: "ckeditor5/latest/features/ai/ai-assistant/ai-assistant-overview.html" ## AI Assistant (legacy) AI Assistant provides a way to boost your editing efficiency and creativity through the use of AI (“artificial intelligence”) capabilities. Users can generate new content and process the data using custom queries, or choose an action from the predefined list of commands, which you can also configure to your liking. ### Demo * Select some content and press the **“AI Commands”** button in the toolbar to get access to the most common tasks such as “Improve writing” or “Summarize.” * Press the **“AI Assistant”** button and provide your query for the AI. Here are a few ideas you might try: * ”Write a detailed guide for a one-day walking tour of Barcelona.” * ”List the top 5 keywords related to traveling.” * Select the table in the content below, then ask: “Fill empty cells with correct values.” This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### UI colors (color coding) The distinctive violet tint used in the AI Assistant’s user interface sets it apart from other (non-AI) features within the CKEditor 5 ecosystem. This use of color coding enhances user experience and provides clear visual cues. Thanks to this, users can easily tell which actions are AI-powered and what results they can expect. All future AI-driven functionalities in CKEditor 5 will also use this color-coding approach. This includes possible smaller AI-related integrations in the existing features. You can [remove](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--removing-the-violet-tint-from-the-ui) or [customize](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--using-custom-colors-for-the-ui) the violet tint to meet the needs of your integration. ### Integration AI Assistant relies on an external service to provide AI-generated responses. You will need to have access to such a service. The feature supports integration with AI API providers: OpenAI and Azure OpenAI. You can also integrate it with custom models. Read the AI Assistant [integration guide](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html) to learn more. ### Customization You can customize the feature to better fit your needs, for example: * Change the list of the predefined commands by adding new ones or removing existing ones. * Change the UI colors, styling, and sizes. Read the AI Assistant [configuration and styling guide](#ckeditor5/latest/features/ai/ai-assistant/ai-assistant-integration.html--configuration-and-styling) to learn more. ### Data filtering in responses The AI response will only incorporate HTML elements and features that are compatible with the editor. For instance, if an AI Assistant response contains `Bold text`, your editor needs the [Bold](../../../api/module%5Fbasic-styles%5Fbold-Bold.html) plugin to be loaded to preserve it. ### Query history The AI Assistant’s custom query field has a dedicated history button that gives you access to your previous queries. It displays the last 20 queries and makes it possible to reuse and improve recent queries. The history is stored in the browser’s [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). The session storage is separate for each browser tab and the browser clears it when you close the tab. A user can also manually clear the query history by clicking the “Clear” button in the history dropdown. ### Known issues and caveats **Integration with some features** The AI Assistant feature will be inactive if the current selection contains one of the following elements: media embed, HTML embed, or table of contents. You can overwrite the list of features where the AI Assistant is inactive by changing the [config.ai.assistant.disabledElements](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-disabledElements) configuration property. **Comments and suggestions** When the AI model processes the selected content, it removes all markers. This means all comments and suggestions will be removed in the AI response. **Broken images in the responses** The AI model, if asked directly, may return image elements linking to non-existing images, which will appear broken in the response and in the editor content after being inserted. **Integration with custom features** Due to the nature of the technology behind this feature, you may get unexpected results when processing content that includes custom features that you have developed. The AI model will try to process the HTML data according to how it was trained. However, if your feature creates a complex HTML structure or custom HTML tags, there is a higher chance that these will not be processed correctly. If you experience problems with your custom features, you might turn off AI Assistant when they are selected by changing the [config.ai.assistant.disabledElements](../../../api/module%5Fai%5Faiassistant%5Faiassistant-AIAssistantConfig.html#member-disabledElements) configuration property. ### Related features Here are some more CKEditor 5 features that can help you boost productivity: * [CKEditor AI](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html) – CKEditor AI provides a way to boost your editing efficiency and creativity through the use of AI. * [Automatic text transformations](#ckeditor5/latest/features/text-transformation.html) – Automatically change predefined text fragments into their improved forms. ### Common API The [AIAssistantUI](../../../api/module%5Fai%5Faiassistant%5Faiassistantui-AIAssistantUI.html) plugin registers: * The `'aiCommands'` UI dropdown component. * The `'aiAssistant'` UI button component. * The ['showAIAssistant' command](../../../api/module%5Fai%5Faiassistant%5Fui%5Fshowaiassistantcommand-ShowAIAssistantCommand.html). You can execute the command to display the AI Assistant: ``` editor.execute( 'showAIAssistant' ); ``` You can also display the AI Assistant and automatically submit the query to the AI service: ``` editor.execute( 'showAIAssistant', 'The query sent to the AI service', 'Label to display in the query field' ); ``` source file: "ckeditor5/latest/features/ai/ckeditor-ai-actions.html" ## AI Quick Actions AI Quick Actions streamline routine content transformations by offering one-click AI-powered suggestions directly within the editor. You can also ask questions about your selected text in the Chat to get instant AI insights and analysis. This feature enhances speed, relevance, and usability, particularly for repeatable or simple tasks. The feature comes with an easy-to-use window interface but can also act as a conversation starter with the [Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html). ### Demo This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. ### Integration To start using the Quick actions feature, first load the `AIQuickActions` plugin in your editor configuration. [Learn more about installing and enabling AI features](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html). Then, you can add the menu that opens the list of Quick actions (`'aiQuickActions'`) to your main toolbar and/or balloon toolbar configurations. To learn more about toolbar configuration, refer to the [toolbar configuration](#ckeditor5/latest/getting-started/setup/toolbar.html) guide. Finally, you can also add individual Quick actions to the toolbar as shortcuts for even easier access. For example, you can add the `'ask-ai'` button, or the `'improve-writing'` button (find it in the demo above). You can add whole categories to the toolbar, too. [Learn more about available actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html--default-actions). The final example configuration looks as follows: ``` ClassicEditor .create( { /* ... */ plugins: [ AIQuickActions, /* ... */ ], ai: { /* ... */ }, // Adding Quick action to the main editor toolbar. toolbar: [ // The main Quick actions button 'aiQuickActions', // Two individual actions 'ask-ai', 'improve-writing', // Whole action category 'translate', /* ... */ ], // Adding Quick Actions to the balloon toolbar. Since some of the actions are selection-sensitive, // accessing them might be easier for users using this kind of toolbar. balloonToolbar: { items: [ // The main Quick actions button 'aiQuickActions', // Two individual actions 'ask-ai', 'improve-writing', // Whole action category 'translate', /* ... */ ], } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Types of actions There are two types of actions available in the Quick actions feature: * Some actions, for instance, “Ask AI” or “Summarize”, lead to the [Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) interface with selected text added as context. The former will just open the Chat and allow you to start typing your message. The latter, however, will not only open the Chat but also start the conversation for your current editor selection right away, and expect a summary of that selection from the AI. * Executing other actions like “Continue writing” or “Make shorter” will open the window interface conveniently right next to your selection and present the answers from the AI for you to accept or reject them. You can define the behavior of each action when you [create custom ones](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html--custom-actions). ### Default actions By default, the Quick actions feature includes several built-in actions that speed up the content editing process. All Quick actions can be accessed through the menu button (`'aiQuickActions'`) but also individually when handpicked by the integrator in the [editor toolbar configuration](#ckeditor5/latest/getting-started/setup/toolbar.html). You can add the whole action categories to the toolbar too. Keep in mind that you can [add custom actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html--custom-actions) to the list and [remove defaults](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html--removing-default-actions). Here’s the full list of available actions: * `'ask-ai'`, * “Chat commands” category (`'chat-commands'`) * `'explain'`, * `'summarize'`, * `'highlight-key-points'`, * `'improve-writing'`, * `'continue'`, * `'fix-grammar'`, * “Adjust length” category (`'adjust-length'`) * `'make-shorter'`, * `'make-longer'`, * “Change tone” category (`'change-tone'`) * `'make-tone-casual'`, * `'make-tone-direct'`, * `'make-tone-friendly'`, * `'make-tone-confident'`, * `'make-tone-professional'`, * “Translate” category (`'translate'`) * `'translate-to-english'`, * `'translate-to-chinese'`, * `'translate-to-french'`, * `'translate-to-german'`, * `'translate-to-italian'`, * `'translate-to-portuguese'`, * `'translate-to-russian'` ### Custom actions The [config.ai.quickActions.extraCommands](../../api/module%5Fai%5Faiquickactions%5Faiquickactions-AIQuickActionsConfig.html#member-extraCommands) property allows you to add new commands to the AI Quick Actions feature. Below, you will find an example of three extra actions added to the user interface: two of them open the quick actions window, but the last one interacts with the Chat. Learn more about [types of actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html--types-of-actions). ``` ClassicEditor .create( { /* ... */ plugins: [ AIQuickActions, /* ... */ ], ai: { quickActions: { extraCommands: [ { id: 'add-quote-from-famous-person', label: 'Add a quote from a famous person', prompt: 'Add a quote from a known person, which would make sense in the context of the selected text.', type: 'action', model: 'claude-4-sonnet' }, { id: 'summarize-in-bullet-points', label: 'Summarize', displayedPrompt: 'Summarize in 5 bullet points', prompt: 'Summarize the selected text in 5 bullet points.', type: 'chat' }, { id: 'include-more-sarcasm', label: 'Rewrite adding more sarcasm', prompt: 'Rewrite using a sarcastic tone.', type: 'action', model: 'claude-4-sonnet' } // ... More commands ... ], }, /* ... */ } } ) .then( ... ) .catch( ... ); ``` ### Removing default actions The [config.ai.quickActions.removeCommands](../../api/module%5Fai%5Faiquickactions%5Faiquickactions-AIQuickActionsConfig.html#member-removeCommands) property allows you to remove existing commands from the AI Quick Actions feature. Here’s an example that removes two actions (”Explain” and “Summarize”): ``` ClassicEditor .create( { /* ... */ plugins: [ AIQuickActions, /* ... */ ], ai: { quickActions: { removeCommands: [ 'explain', 'summarize', // ... More commands to remove ... ] }, /* ... */ } } ) .then( ... ) .catch( ... ); ``` ### Common API Quick Actions can be triggered programmatically – execute system actions or custom prompts from code. | API | Description | | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | [AIActions#executeAction()](../../api/module%5Fai%5Faiactions%5Faiactions-AIActions.html#function-executeAction) | Execute a system action or a custom prompt on the current selection. | | [AIActionDefinition](../../api/module%5Fai%5Faiactions%5Faiactions-AIActionDefinition.html) | Type defining the parameters for an AI action (system actionName or custom userMessage). | | [AIActionsNames](../../api/module%5Fai%5Faiactions%5Faiactions-AIActionsNames.html) | Enum of available system action names. | See the [programmatic usage guide](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html--quick-actions) for details, examples, and a live demo. #### REST API Quick Actions are also available via the [Actions REST API](#cs/latest/guides/ckeditor-ai/actions.html) for use outside the editor. Use this to run stateless content transforms like fixing grammar, improving writing, or running custom prompts from your backend or frontend code. See the [programmatic documentation](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html) for examples and the [full API reference](https://ai.cke-cs.com). source file: "ckeditor5/latest/features/ai/ckeditor-ai-chat.html" ## AI Chat The AI Chat is a conversational AI that can aid content creation and editing. It introduces a dynamic chat interface designed to facilitate rich, multi-turn interactions between users and AI, enabling an interactive and collaborative experience within writing workflows. ### Demo This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. ### Key capabilities #### Working with the document CKEditor AI operates directly within the context of your document. When you chat with it, you can ask questions about specific sections, request a full-document proofreading, and more. By enabling [Web search](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--web-search) or [Reasoning](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--reasoning), you can extend its capabilities — allowing the chat to look up information online and tackle complex tasks step by step. #### Making changes to the content Not only can you chat with the AI, but you can also use it to introduce changes to your document. Ask it to _“Summarize the document”_, _“Turn this report into a one-page executive summary”_, or _“Suggest better section titles and subheadings”_. The AI will then propose a series of changes to the document you can [review](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--previewing-changes) and [accept or discard one by one](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--applying-changes). **Copying and pasting chat transcripts is over; CKEditor AI understands your content and edits with you hand in hand**. #### Brainstorming The chat feature jump-starts your creative process. Begin with a blank document and ask the AI for ideas. Build your content step by step by chatting and applying changes. Then review – or have the AI rewrite – the final draft for best results. All in one place. ### Integration To start using the Chat feature, load the `AIChat` plugin in your editor configuration. The Chat button will appear in the AI user interface along with the Chat history . [Learn more about installing and enabling AI features](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html). ### Available models Users can select the desired AI model for their conversation from a dropdown at the bottom of the chat. Once selected, the AI model will persist for the duration of the conversation. If you want to change the model, you can start a new conversation using a dedicated \+ New chat button at the top-right corner of the chat panel. #### Web search Web search in Chat allows it to access and retrieve real-time information from the internet. Instead of relying only on pre-trained knowledge, the model can search the web to find up-to-date facts, verify details, and provide more accurate, current answers. Toggle the “Enable web search” button for a compatible model to start using the Web search functionality. #### Reasoning Reasoning in Chat models turns on the ability to think through problems, draw logical conclusions, and make sense of complex information. It enables the model to analyze context, connect ideas, and produce well-structured, coherent answers beyond simple pattern matching. Toggle the “Enable reasoning” button for a compatible model to start using Reasoning. #### Configuration The optional [config.ai.models](../../api/module%5Fai%5Faiconfig-AIConfig.html#member-models) setting controls the models available to the users across all AI features (Chat and Review Mode). The property lets you set the default model, tailor the available models list, and control the model selector UI visibility. [Learn more about available AI models](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--supported-ai-models). ``` ClassicEditor .create( { /* ... */ plugins: [ AIChat, AIEditorIntegration, /* ... */ ], ai: { models: { defaultModelId: 'claude-3-5-haiku', displayedModels: [ 'gpt', 'claude' ], showModelSelector: false } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Adding context to conversations The AI chat can work with your document and beyond. Use the “Add context” button on the right of the prompt input, to add URLs, files, and external resources to your conversation. Ask the AI about specific resources, for instance, _“Describe the attached image”_ or _“Summarize the key points from the attached Word document”_. The AI will analyze those resources for you and provide information you can easily use in your document. External resources enable you to seamlessly integrate knowledge bases and other centralized data into your AI Chat conversations. Instead of uploading documents each time you want to chat, you can simply select them from a list and reference them during your conversation. Learn more about [configuring resources](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--configuring-conversation-context) in AI Chat. #### Configuring conversation context The [config.ai.chat.context](../../api/module%5Fai%5Faichat%5Faichat-AIChatConfig.html#member-context) property configures the AI Chat menu for adding resources to the prompt context. The example below enables built-in options to add the current [document](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-document), [URLs](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-urls), and [files](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-files) to the conversation context by the user. It also demonstrates a [context sources configuration](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-sources) that introduces a menu with external resources fetched from a database or an external API for the user to pick from. You can learn more about the configuration of a [custom context providers](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIContextProvider.html) in the documentation. See the example configuration below: ``` ClassicEditor .create( { /* ... */ plugins: [ AIChat, AIEditorIntegration, /* ... */ ], ai: { chat: { context: { // Allow for adding the current document to the conversation. document: { enabled: true }, // Allow for adding URLs to the conversation. urls: { enabled: true }, // Allow for uploading files to the conversation. files: { enabled: true }, // External resources configuration. sources: [ // Definition of the custom context provider. { // The unique identifier of the provider. id: 'my-docs', // The human-readable name of the provider. label: 'My Documents', // The async callback to retrieve the list of available resources. // Usually involves fetching data from a database or an external API, // but here we use a simple array of resources for demonstration purposes. getResources: async ( query ) => [ // Text resources. { id: 'text3', type: 'text', label: 'Internal note in HTML format', data: { content: '

HTML note

Lorem ipsum dolor sit amet...

', type: 'html' } }, { id: 'text4', type: 'text', label: 'Internal note (fetched on demand)', // Note: Since the `data` property is not provided, the content will be retrieved using the `getData()` callback (see below). // This will prevent fetching large content along with the list of resources. }, // URLs to resources. { id: 'url2', type: 'web-resource', label: 'Company brochure in PDF', data: 'https://example.com/brochure.pdf' }, { id: 'url3', type: 'web-resource', label: 'Company website in HTML', data: 'https://example.com/index.html' }, // ... ], // The optional callback to retrieve the content of resources without the `data` property provided by the `getResources()` callback. // When the user picks a specific resource, the content will be fetched on demand (from database or external API) by this callback. // This prevents fetching large resources along with the list of resources. getData: ( id ) => fetchDocumentContent( id ) }, // More context providers... ] }, } } } ) .then( /* ... */ ) .catch( /* ... */ ); ```
#### Automatically add selection to context By default, the editor selection is added to the AI Chat context only when the user explicitly clicks the “Ask AI” button. You can change this behavior by enabling the [config.ai.chat.context.alwaysAddSelection](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-alwaysAddSelection) option. When set to `true`, the current editor selection is automatically added to the conversation context whenever the user changes their selection. If the selection becomes collapsed (empty), it is automatically removed from the context. This option requires the [document](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-document) context to be enabled (which is the default). ``` ClassicEditor .create( document.querySelector( '#editor' ), { /* ... */ plugins: [ AIChat, AIEditorIntegration, /* ... */ ], ai: { chat: { context: { alwaysAddSelection: true } } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Adding context via custom context provider It’s also possible to allow for adding the context using a custom external UI, for example, a file manager. First, using the [config.ai.chat.context.customItems](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContextConfig.html#member-customItems) configuration option, add a button to the “Add context” dropdown that upon pressing will execute the configured [callback](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIContextCustomItem.html#member-callback) (for example, open custom content manager). Then you can use the [AIChatContext class API](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContext.html) to attach the resource chosen by the user to the conversation context. There are various API methods to use depending on the context type. See an example code below. ``` ClassicEditor .create( { /* ... */ plugins: [ AIChat, AIEditorIntegration, /* ... */ ], ai: { chat: { context: { // Other configuration options... customItems: [ { id: 'open-file-manager', // Unique item ID. label: 'Open file manager', // Button label displayed in the dropdown. icon: IconImage, // Icon displayed next to the label. callback: ( editor: Editor ) => { // `openFileManager()` is provided by you. It opens a custom UI, and returns a promise. // After the user finishes choosing files, the `openFileManager()` promise resolves with data of these files. openFileManager().then( chosenFiles => { editor.plugins.get( 'AIChatController' ).activeConversation.chatContext.addFilesToContext( chosenFiles ); } ); } }, { id: 'open-url-manager', // Unique item ID. label: 'Open URL manager', // Button label displayed in the dropdown. icon: IconURL, // Icon displayed next to the label. callback: ( editor: Editor ) => { // `openURLManager()` is provided by you. It opens a custom UI, and returns a promise. // After the user finishes choosing a URL, the `openURLManager()` promise resolves with proper data. openURLManager().then( chosenURL => { editor.plugins.get( 'AIChatController' ).activeConversation.chatContext.addURLToContext( chosenURL ); } ); } } ] }, } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Welcome message The AI Chat feature allows you to customize the welcome message displayed to users when the chat initializes. You can set the [config.ai.chat.welcomeMessage](../../api/module%5Fai%5Faichat%5Faichat-AIChatConfig.html#member-welcomeMessage) option in your editor configuration to provide a custom message. If this option is not set, a default welcome message will be shown. ``` ClassicEditor.create( { /* ... */ plugins: [ AIChat, AIEditorIntegration, /* ... */ ], ai: { chat: { welcomeMessage: 'Hello! How can I assist you today?' // More configuration options... } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Working with AI-generated changes If you ask the AI for changes to your document, for instance, _“Bold key facts in the document”_, you will receive a series of proposed changes instead of plain text responses: Move your cursor over any change to highlight the section of your document it applies to, helping you identify it among other proposed edits. #### Showing details You can toggle details of the changes by pressing the “Show details” button. By default, you will see detailed information on what exactly was suggested, including additions (green markers), removals (red markers), and formatting changes (blue markers). Click the button again to see a clean, simplified overview of the changes as they’ll appear in your document once accepted. #### Previewing changes Click on the item in the list to display the information window about an individual change with options to [apply it](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--applying-changes), [turn it into a Track Changes suggestion](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--inserting-track-changes-suggestions), or [reject it](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--rejecting-suggestions). You can use this window to browse all proposed changes and work with them one by one. As you navigate through the changes, the window will automatically follow the corresponding sections of the document. #### Applying changes Each suggestion on the list comes with an “Apply” button that allows you to apply the change to the document immediately. Click the “Apply all” button in chat to apply all AI suggestions at once. #### Inserting Track Changes suggestions [When Track Changes feature is available in your integration](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--track-changes-dependency), the “Add as suggestion” button will be available in chat. Clicking it will create a Track Changes suggestion that can later be reviewed or discarded. You can pick the “Suggest all” option under the list to turn all changes suggested by AI into Track Changes suggestions. #### Rejecting suggestions You can click the “Reject change” button to reject AI suggestions you do not want before applying the remaining ones or turning them into Track Changes suggestions. ### Chat history All your past conversations appear in the Chat history . Click the button to open the list, where you can reopen, rename, or delete any conversation. Conversations are grouped by date to help you navigate your project easily. You can filter conversations by name using the search field at the top of the user interface. ### Chat Shortcuts The AI Chat feature can be enhanced by AI Chat Shortcuts – customizable actions that help users trigger common or useful prompts with a single click. These shortcuts appear at the start of a new conversation, making it faster for users to ask questions, request summaries, check grammar, and more. AI Chat Shortcuts require loading the [AIChatShortcuts](../../api/module%5Fai%5Faichatshortcuts%5Faichatshortcuts-AIChatShortcuts.html) plugin in your editor configuration. You can configure which shortcuts are available using the [config.ai.chat.shortcuts](../../api/module%5Fai%5Faichat%5Faichat-AIChatConfig.html#member-shortcuts) option. This allows you to define shortcut labels, icons, prompts, and the type of action to execute. Shortcuts streamline repetitive queries and encourage best practices in your writing workflows. Example configuration: ``` import { AIChat, AIChatShortcuts, AIEditorIntegration, /* ... */ } from 'ckeditor5-premium-features'; ClassicEditor.create( { /* ... */ // Adding the AIChatShortcuts plugin to enable the feature. plugins: [ AIChat, AIChatShortcuts, AIEditorIntegration, /* ... */ ], /* ... */ ai: { chat: { shortcuts: [ // This shortcut runs an AI Chat prompt with Reasoning and // Web Search features turned on. { id: 'continue-writing', type: 'chat', label: 'Continue writing', prompt: 'Continue writing this document. Match the existing tone, vocabulary level, and formatting. ' + 'Do not repeat or summarize earlier sections. Ensure logical flow and progression of ideas. ' + 'Add approximately 3 paragraphs.', useReasoning: true, useWebSearch: true }, // This shortcut starts proofreading the document by the AI Review feature. { id: 'fix-grammar-and-spelling', type: 'review', label: 'Fix grammar and spelling', commandId: 'correctness' }, // This shortcut switches the UI to the Translate feature and allows // the user decide what to do next (choose a language). { id: 'translate-document', type: 'translate', label: 'Translate document' } ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Common API AI Chat can be controlled programmatically – send messages, start conversations, and manage chat context from code. | API | Description | | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | [AIChatController#sendMessage()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-sendMessage) | Programmatically send a message to AI Chat. | | [AIChatController#startConversation()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-startConversation) | Start a new chat conversation. | | [AIChatController#addSelectionToChatContext()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-addSelectionToChatContext) | Attach the current editor selection as context for the next message. | See the [programmatic usage guide](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html--chat) for details, examples, and a live demo. #### REST API AI Chat conversations are also available via the [Conversations REST API](#cs/latest/guides/ckeditor-ai/conversations.html), which supports multi-turn conversation history, file uploads, and web search capabilities. Use this to build chat-based AI features outside the editor. See the [programmatic documentation](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html) for examples and the [full API reference](https://ai.cke-cs.com). source file: "ckeditor5/latest/features/ai/ckeditor-ai-deployment.html" ## Deployment options CKEditor AI backend is available in two deployment modes: **Cloud (SaaS)** and **On-premises**. Both options provide the same core AI features – [Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html), [Quick Actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html), [Review](#ckeditor5/latest/features/ai/ckeditor-ai-review.html), and [Translate](#ckeditor5/latest/features/ai/ckeditor-ai-translate.html) – with the on-premises version offering additional capabilities such as custom AI models and [MCP support](#ckeditor5/latest/features/ai/ckeditor-ai-mcp.html). ### Cloud (SaaS) The Cloud (SaaS) deployment offers the fastest way to get started with CKEditor AI. The AI service is hosted and managed by CKEditor, so there is no server-side setup required on your end. You only need to provide a valid license key and configure the editor-side plugins as described in the [integration guide](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html). For more information about the Cloud AI service, refer to the [CKEditor AI Cloud Services documentation](#cs/latest/guides/ckeditor-ai/overview.html). ### On-premises The on-premises deployment allows you to run the CKEditor AI service on your own infrastructure, including private cloud environments. The service is distributed as Docker images compatible with standard container runtimes. On-premises deployment gives you full control over the AI service, including the ability to use custom AI models and providers, and to extend CKEditor AI with custom tools via [MCP (Model Context Protocol)](#ckeditor5/latest/features/ai/ckeditor-ai-mcp.html). For detailed setup instructions, requirements, and configuration, refer to the [CKEditor AI On-Premises documentation](#cs/latest/onpremises/ckeditor-ai-onpremises/overview.html). #### Connecting the editor to an on-premises service To point the editor to your on-premises AI service, set the [config.ai.serviceUrl](../../api/module%5Fai%5Faiconfig-AIConfig.html#member-serviceUrl) property to the URL of your on-premises instance: ``` ClassicEditor .create( { licenseKey: '', ai: { serviceUrl: 'https://your-on-prem-host.com/v1', // ... Other AI configuration options. } // ... Other editor configuration. } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Custom AI models The on-premises version supports custom AI model providers, allowing you to use your own models hosted on services like Google Cloud, Microsoft Azure, or self-hosted solutions. Models configured on the server side will automatically appear in the editor’s model selector. For configuration details, refer to the [on-premises configuration guide](#cs/latest/onpremises/ckeditor-ai-onpremises/configuration.html--custom-models). #### MCP support The on-premises deployment supports the Model Context Protocol (MCP), which allows you to extend CKEditor AI with custom external tools. Learn more in the [MCP support](#ckeditor5/latest/features/ai/ckeditor-ai-mcp.html) guide. ### Feature comparison | Feature | Cloud (SaaS) | On-premises | | ----------------------------------------------------------------- | ------------ | ----------- | | Hosted and managed by CKEditor | ✅ Yes | ❌ No | | Custom infrastructure | ❌ No | ✅ Yes | | Custom AI models and providers | ❌ No | ✅ Yes | | [MCP support](#ckeditor5/latest/features/ai/ckeditor-ai-mcp.html) | ❌ No | ✅ Yes | source file: "ckeditor5/latest/features/ai/ckeditor-ai-integration.html" ## Integrating CKEditor AI with your application This guide will take you step-by-step through the process of running CKEditor AI in your editor integration. It also presents possible configuration and customization options. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and provide [essential configuration](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--sample-implementation): #### Enabling individual features Each AI feature is a standalone plugin that you can include or exclude from the `plugins` array depending on your needs. For example, to use only AI Review and AI Translate without Chat or Quick Actions: ``` import { ClassicEditor } from 'ckeditor5'; import { AIReviewMode, AITranslate, AIEditorIntegration, TrackChanges } from 'ckeditor5-premium-features'; ClassicEditor .create( { licenseKey: '', plugins: [ AIReviewMode, AITranslate, AIEditorIntegration, TrackChanges, /* ... */ ], ai: { container: { /* ... */ } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` The following table lists the plugins responsible for each feature: | Feature | Plugin | Description | | ----------------- | --------------- | --------------------------------------------- | | AI Chat | AIChat | Conversational AI assistant | | AI Chat Shortcuts | AIChatShortcuts | Predefined shortcuts displayed in the AI Chat | | AI Quick Actions | AIQuickActions | One-click AI-powered content transformations | | AI Review | AIReviewMode | AI-powered quality assurance checks | | AI Translate | AITranslate | AI-powered document translation | ### Sample implementation An example CKEditor AI configuration is presented below. You can learn more about specific configurations such as [UI types and positioning](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--ui-types-and-positioning) or [Track Changes dependency](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--track-changes-dependency) in the later sections of this guide. To learn more about toolbar configuration, refer to the [toolbar configuration](#ckeditor5/latest/getting-started/setup/toolbar.html) guide. ``` // Simplified integration of the Users plugin needed for TrackChanges integration. class UsersIntegration extends Plugin { static get requires() { return [ 'Users' ]; } init() { const users = this.editor.plugins.get( 'Users' ); // Just add a minimal dummy user users.addUser( { id: 'user-1', name: 'John Doe' } ); users.defineMe( 'user-1' ); } } ClassicEditor .create( { licenseKey: '', plugins: [ AIChat, AIQuickActions, AIReviewMode, AITranslate, AIEditorIntegration, AIChatShortcuts, TrackChanges, UsersIntegration, /* ... */ ], // Extend the main editor toolbar configuration with additional buttons: // - 'aiQuickActions': opens the AI Quick Actions menu, // - 'ask-ai': moves the user focus to the AI Chat, // - 'improve-writing': executes the "Improve Writing" quick action. // // You can add more AI Quick actions to the toolbar configuration if needed. toolbar: [ 'aiQuickActions', 'ask-ai', 'improve-writing', /* ... */ ], // You can use the same AI feature buttons in the balloon toolbar configuration for contextual convenience. balloonToolbar: { items: [ /* ... */ 'aiQuickActions', 'ask-ai', 'improve-writing', /* ... */ ] }, // Configure the document identifier for AI chat history and context preservation. // This should be a unique identifier for the document/article being edited. collaboration: { channelId: 'channelId' // Replace with your actual document ID }, // Main configuration of AI feature. ai: { // ⚠️ Mandatory UI configuration. // Display the AI user interface in a dedicated DOM element. The interface can be also displayed // in an overlay or in a custom way, learn more in the next chapters of this guide. container: { type: 'sidebar', element: document.querySelector( '.ai-sidebar' ), // (Optional) Whether the AI interface should be visible when the editor is created. visibleByDefault: false }, // (Optional) Configure the AI Chat feature by configuring available context resources. chat: { // (Optional) Configure AI Chat Shortcuts that appear at the start of a new conversation. shortcuts: [ { id: 'continue-writing', type: 'chat', label: 'Continue writing', prompt: 'Continue writing this document. Match the existing tone, vocabulary level, and formatting. ' + 'Do not repeat or summarize earlier sections. Ensure logical flow and progression of ideas. ' + 'Add approximately 3 paragraphs.' } ], context: { // Configuration of the built-in context options. document: { enabled: true }, urls: { enabled: false }, files: { enabled: true }, // (Optional) Additional sources for the AI Chat context. sources: [ // Definition of the custom context provider. { // The unique identifier of the provider. id: 'my-docs', // The human-readable name of the provider. label: 'My Documents', // The async callback to retrieve the list of available resources. // Usually involves fetching data from a database or an external API, // but here we use a simple array of resources for demonstration purposes. getResources: async ( query ) => [ // Texts in various formats { id: 'text1', type: 'text', label: 'Internal note in plain text format', data: { content: 'Lorem ipsum dolor sit amet...', type: 'text' } }, { id: 'text2', type: 'text', label: 'Internal note in Markdown format', data: { content: '## Markdown note\n\n**Lorem ipsum** dolor sit amet...', type: 'markdown' } }, { id: 'text3', type: 'text', label: 'Internal note in HTML format', data: { content: '

HTML note

Lorem ipsum dolor sit amet...

', type: 'html' } }, { id: 'text4', type: 'text', label: 'Internal note (fetched on demand)', // Note: Since the `data` property is not provided, the content will be retrieved using the `getData()` callback (see below). // This will prevent fetching large content along with the list of resources. }, // URLs to resources in different formats { id: 'url1', type: 'web-resource', label: 'Blog post in Markdown', data: 'https://example.com/blog-post.md' }, { id: 'url2', type: 'web-resource', label: 'Company brochure in PDF', data: 'https://example.com/brochure.pdf' }, { id: 'url3', type: 'web-resource', label: 'Company website in HTML', data: 'https://example.com/index.html' }, { id: 'url4', type: 'web-resource', label: 'Terms of service in plain text', data: 'https://example.com/terms-of-service.txt' }, // ... ], // The optional callback to retrieve the content of resources without the `data` property provided by the `getResources()` callback. // When the user picks a specific resource, the content will be fetched on demand (from database or external API) by this callback. // This prevents fetching large resources along with the list of resources. getData: ( id ) => fetchDocumentContent( id ) }, // More context providers... ] } }, // (Optional) The configuration for AI models used across all AI features (Chat and Review Mode). models: { defaultModelId: 'gpt-5', displayedModels: [ 'gpt', 'claude' ], showModelSelector: true }, // (Optional) Configure the AI Quick Actions feature by adding a new command. quickActions: { extraCommands: [ // An action that opens the AI Chat interface for interactive conversations. { id: 'explain-like-i-am-five', displayedPrompt: 'Explain like I am five', prompt: 'Explain the following text like I am five years old.', type: 'CHAT' }, // ... More custom actions ... ], }, } } ) .then( /* ... */ ) .catch( /* ... */ ); ```
### Configuration #### Supported AI models CKEditor AI supports OpenAI, Anthropic, and Gemini AI models. By default, the automatically selected model will be used for optimal cost and performance. You can configure the list of available models and the default model using the unified [model configuration](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--configuration), which applies to all AI features (Chat and Review Mode). Here’s a detailed list of available models with their capabilities: | **Model** | **Description** | [Web Search](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--web-search) | [Reasoning](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--reasoning) | [Configuration id](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--configuration) | | --------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Auto (default)** | Automatically selects best model for speed, quality, and cost. | Yes | Yes | 'auto' (also 'agent-1', learn more about [compatibility versions](#cs/latest/guides/ckeditor-ai/models.html--model-compatibility-versions)) | | **GPT-5.4** | OpenAI’s flagship model for advanced reasoning, creativity, and complex tasks | Yes | Yes | 'gpt-5.4' | | **GPT-5.2** | OpenAI’s flagship model for advanced reasoning, creativity, and complex tasks | Yes | Yes | 'gpt-5.2' | | **GPT-5.1** | OpenAI’s flagship model for advanced reasoning, creativity, and complex tasks | Yes | Yes | 'gpt-5.1' | | **GPT-5** | OpenAI’s flagship model for advanced reasoning, creativity, and complex tasks | Yes | Yes | 'gpt-5' | | **GPT-5 Mini** | A lightweight version of GPT-5 – faster, more cost-efficient | Yes | Yes | 'gpt-5-mini' | | **Claude 4.6 Sonnet** | Advanced model with improved creativity, reliability, and reasoning | Yes | Yes | 'claude-4-6-sonnet' | | **Claude 4.5 Haiku** | Cost-efficient model for quick interactions with improved reasoning | Yes | Yes | 'claude-4-5-haiku' | | **Claude 4.5 Sonnet** | Advanced model with improved creativity, reliability, and reasoning | Yes | Yes | 'claude-4-5-sonnet' | | **Gemini 3.1 Pro** | Google’s advanced model for versatile problem-solving and research | Yes | Yes | 'gemini-3-1-pro' | | **Gemini 3 Flash** | Lightweight Gemini model for fast, cost-efficient interactions | Yes | Yes | 'gemini-3-flash' | | **Gemini 2.5 Flash** | Lightweight Gemini model for fast, cost-efficient interactions | Yes | Yes | 'gemini-2-5-flash' | | **GPT-4.1** | OpenAI’s model for reliable reasoning, speed, and versatility | Yes | No | 'gpt-4.1' | | **GPT-4.1 Mini** | A lighter variant of GPT-4.1 that balances speed and cost while maintaining solid accuracy | Yes | No | 'gpt-4.1-mini' | We intend to expand this list of supported agents further with time. The [on-premises version](#ckeditor5/latest/features/ai/ckeditor-ai-deployment.html) also supports custom models and your own API keys. [Share your feedback on model availability](https://ckeditor.com/contact/). #### Cloud version endpoint While using the cloud version of the CKEditor AI feature, you need to provide the service endpoint. ``` ai: { serviceUrl: 'https://ai.cke-cs.com/v1' } ``` If you are using the EU cloud region, remember to adjust the endpoint: ``` ai: { serviceUrl: 'https://ai.cke-cs-eu.com/v1' } ``` #### Document ID The [config.collaboration.channelId](../../api/module%5Fcollaboration-core%5Fconfig-RealTimeCollaborationConfig.html#member-channelId) configuration serves as the document identifier corresponding to the edited resource (article, document, etc.) in your application. This ID is essential for maintaining [Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) history, ensuring that AI conversations are properly associated with the specific document being edited. When users interact with AI features, their chat history is preserved and linked to this document ID. ``` ClassicEditor .create( { /* ... */ collaboration: { channelId: 'DOCUMENT_ID' }, /* ... */ } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Track Changes dependency CKEditor AI can leverage the TrackChanges plugin to enhance the user experience, for instance, by allowing users to turn AI-generated content into suggestions that can later be reviewed, accepted, or rejected. Without the TrackChanges plugin, the CKEditor AI will work, but some functionalities may be limited. For the most complete integration, we highly recommend using TrackChanges along with CKEditor AI. #### UI types and positioning CKEditor AI gives you flexible options for displaying the AI user interface. The [config.ai.container](../../api/module%5Fai%5Faiconfig-AIConfig.html#member-container) property allows you to choose from three different UI placement modes: ##### Sidebar When in [AIContainerSidebar](../../api/module%5Fai%5Faiconfig-AIContainerSidebar.html) mode, the AI user interface is displayed in a specific DOM element, allowing you to inject it into your existing user interface. ``` ClassicEditor .create( { // ... Other configuration options ... ai: { container: { type: 'sidebar', // Existing DOM element to use as the container for the AI user interface. element: document.querySelector( '#ai-sidebar-container' ) // (Optional) The preferred side for positioning the tab buttons. side: 'right' }, } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` In addition to the above, we recommend using the following or similar CSS to style the sidebar container for the AI user interface (tabs) to render optimally: ``` #ai-sidebar-container .ck.ck-ai-tabs { /* An arbitrary fixed width to limit the space consumed by the AI tabs. */ width: 500px; /* A fixed height that enables vertical scrolling (e.g., in the AI Chat feed). */ height: 800px; } ``` ##### Overlay When in [AIContainerOverlay](../../api/module%5Fai%5Faiconfig-AIContainerOverlay.html) mode, the AI user interface is displayed on top of the page, allowing you to position it on your preferred side. This mode is best suited for integrations with limited space. ``` ClassicEditor .create( { // ... Other configuration options ... ai: { container: { type: 'overlay', side: 'right' }, } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Learn how to [toggle the AI overlay](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--toggling-the-ui) using a dedicated toolbar button. ##### Custom When in [AIContainerCustom](../../api/module%5Fai%5Faiconfig-AIContainerCustom.html) mode, the AI user interface is displayed in a custom way, allowing you to use the building blocks of the AI user interface to create your own and satisfy the specific needs of your application. ``` ClassicEditor .create( { // ... Other configuration options ... ai: { container: { type: 'custom' }, } } ) // A custom integration of the AI user interface placing the tab buttons and panels separately in custom containers. .then( editor => { const tabsPlugin = editor.plugins.get( 'AITabs' ); for ( const id of tabsPlugin.view.getTabIds() ) { const tab = tabsPlugin.view.getTab( id ); // Display tab button and panel in a custom container. myButtonsContainer.appendChild( tab.button.element ); myPanelContainer.appendChild( tab.panel.element ); } } ) .catch( /* ... */ ); ``` #### Toggling the UI The user interface can be easily toggled by the users using the `'toggleAi'` toolbar button. The button becomes available for configuration when the [AIEditorIntegration](../../api/module%5Fai%5Faieditorintegration%5Faieditorintegration-AIEditorIntegration.html) plugin is enabled. The following example shows how to enable the `'toggleAi'` button in the main editor toolbar: ``` import { ClassicEditor } from 'ckeditor5'; import { /* ... */, AIEditorIntegration } from 'ckeditor5-premium-features'; ClassicEditor .create( { licenseKey: '', plugins: [ AIEditorIntegration, /* ... */ ], // Enable the `'toggleAi'` button in the main editor toolbar. toolbar: [ 'toggleAi', /* ... */ ], ai: { container: { // ... }, /* ... */ } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` If you wish to initially hide the overlay until a user opens it with a button, you can use the [dedicated configuration](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--hiding-the-ui-on-initialization). #### Hiding the UI on initialization By default, the AI interface will be visible when the editor is created (and the [related toolbar button](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--toggling-the-ui) will be active). If you wish to have it hidden until the user opens it (e.g. via toolbar button), set [config.ai.container.visibleByDefault](../../api/module%5Fai%5Faiconfig-AIConfig.html#member-container) property to `false`. #### Maximizing the UI The maximize button in the upper-right corner allows changing the width of the CKEditor AI user interface. Users can use this button to interact with the AI features more comfortably, especially while [chatting](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) and interacting with large chunks of content. Clicking this button will toggle the `.ck-ai-tabs_maximized` CSS class on the `.ck-ai-tabs` DOM element. The integrator can then style the geometry of the element based on the specific requirements of the integration. * When the UI is configured in the [sidebar mode](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--sidebar), the decision on how to style the maximized state of the user interface is left to the integrator due to many possible integration types and configurations. * When the UI is configured in the [overlay mode](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--overlay), integrators can override the `--ck-ai-tabs-overlay-width-maximized` CSS custom property to change the width of the overlay. ``` :root { /* The CKEditor AI interface will consume 40% of the space when maximized */ --ck-ai-tabs-overlay-width-maximized: 40%; } ``` #### Permissions Learn more about the permissions system used in CKEditor AI in a [dedicated guide](#cs/latest/guides/ckeditor-ai/permissions.html). ### Chat Learn more about integrating the Chat feature in a [dedicated guide](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html). ### Quick Actions Learn more about integrating the Quick Actions feature in a [dedicated guide](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html). ### Review Learn more about integrating the Review feature in a [dedicated guide](#ckeditor5/latest/features/ai/ckeditor-ai-review.html). ### Translate Learn more about integrating the Translate feature in a [dedicated guide](#ckeditor5/latest/features/ai/ckeditor-ai-translate.html). source file: "ckeditor5/latest/features/ai/ckeditor-ai-mcp.html" ## MCP support The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard for connecting AI models to external tools and data sources. CKEditor AI supports MCP, letting you extend its capabilities with custom tools – such as searching knowledge bases, querying databases, integrating with third-party services, or even providing your own custom AI tools – all accessible directly from the [AI Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html). ### Overview When MCP servers are connected to the on-premises AI service, the AI model can use MCP tools during conversations. The general flow is: 1. The user sends a message in the AI Chat. 2. The AI model may decide to call an MCP tool based on the message and conversation context. 3. The on-premises AI service invokes the tool on the connected MCP server. 4. The tool result is returned to the AI service and may be used to improve the quality and accuracy of the response. 5. The tool result is also sent to the editor, where it can be processed and displayed. You can connect both third-party MCP tools (such as public knowledge-base or database connectors) and your own custom tools. For third-party tools, the integration may be as simple as registering a callback to display their results. For custom tools, you have full control over what data the tool returns and how it is presented in the editor. As an integrator, you can: * Use MCP tools to **return additional data** for the main AI service agent, to make it aware of the knowledge specific to your application. * **Handle tool responses** by registering callbacks that process tool notifications and results, to provide a customized rich user experience specific to your use cases. * **Pass context** to MCP tools alongside user messages, to give your custom tools more input to operate on, beyond the user prompt. ### Server-side configuration MCP servers are configured on the on-premises AI service, not in the CKEditor 5 editor configuration. For server setup, connection options, and other details, refer to the [on-premises MCP configuration guide](#cs/latest/onpremises/ckeditor-ai-onpremises/mcp.html). ### Client-side integration #### Handling MCP tool responses When an MCP tool is called, progress notifications and the final result are passed to the editor. Use [registerToolDataCallback()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-registerToolDataCallback) to process this data and control how it appears in the AI Chat. ``` const aiChatController = editor.plugins.get( 'AIChatController' ); aiChatController.registerToolDataCallback( ( toolData, api ) => { // Handle tool data here. } ); ``` Each callback receives: * `toolData` ([AIToolData](../../api/module%5Fai%5Faicore%5Fmodel%5Faiinteraction-AIToolData.html)) – the tool name, event type, and data payload. The MCP tool is fully responsible for the contents of `toolData.data`. * `api` ([AIChatFeedAPI](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatFeedAPI.html)) – methods for manipulating the chat feed. Use `toolData.type` (`'result'` or `'notification'`) and `toolData.toolName` to route handling logic. Tool names are prefixed with the server name in the format `{serverName}-{toolName}` (for example, `my-server-search-docs`). You can register multiple callbacks to handle different tools separately. ##### Handling tool notifications Tools may send notifications or progress updates, informing about the current state of the call, or sharing additional insight about what the tool does. For these kinds of updates, `toolData.type` will be set to `'notification'`. You can use this data to show a custom loading message or even provide your own “chain-of-thought” component. ``` const aiChatController = editor.plugins.get( 'AIChatController' ); aiChatController.registerToolDataCallback( ( toolData, api ) => { if ( toolData.type === 'notification' ) { api.setLoadingMessage( toolData.data.message ); } } ); ``` Note that tool notifications are treated as temporary data and **are not** saved in the conversation history. They will not be shown when a conversation is loaded from the history. ##### Handling tool results Every tool is expected to return a result after it finishes processing. Tool result data has `toolData.type` set to `'result'`. Tool results are displayed in the chat feed in the order they arrive. If a tool result comes before the AI’s proposed changes to the document, it will appear before them. If it arrives after, it will appear after. Tool results will never split the proposed changes. Depending on your use case, you may simply show the result as text, or provide a custom, rich UI component that will display the returned data as a graph, table, or in any custom way that fits your needs. Unlike the temporary updates from notifications, results are persistent data saved in the conversation history. The data is saved as-received. When a conversation is loaded from history, the same registered callbacks will be used to handle the saved tool data. ###### Displaying tool results as text Use the provided API to insert the text result into the chat feed: ``` const aiChatController = editor.plugins.get( 'AIChatController' ); aiChatController.registerToolDataCallback( ( toolData, api ) => { if ( toolData.type === 'result' ) { api.insertTextReply( toolData.data.summary ); } } ); ``` ###### Building custom UI from structured data When a tool returns structured data (for example, tabular data as JSON), you can build custom HTML and insert it into the chat feed: ``` const aiChatController = editor.plugins.get( 'AIChatController' ); aiChatController.registerToolDataCallback( ( toolData, api ) => { if ( toolData.type === 'result' && toolData.data.rows ) { const headers = toolData.data.columns; const rows = toolData.data.rows; const headerHtml = headers.map( h => `${ h }` ).join( '' ); const rowsHtml = rows.map( row => '' + row.map( cell => `${ cell }` ).join( '' ) + '' ).join( '' ); const tableHtml = `${ headerHtml }${ rowsHtml }
`; api.insertCustomElement( tableHtml ); } } ); ```
#### Passing context to MCP tools MCP tools automatically receive the conversation context when a user sends a message. However, your application often holds additional information that can help the tool produce better results. You can attach this data to messages so that MCP tools can use it. There are two approaches: **message attributes** and **tool context items**. The key difference is in how they are used: * **Message attributes** are metadata that is always present with the message and typically invisible to the end user. They are set programmatically and intended for data that should accompany every message transparently – for example, the user’s tenant ID, department, or session information. * **Tool context items** are reflected in the UI as context pills next to the prompt input (similar to attached files or URLs). They are visible to the user, who can interact with them – add, review, or remove them. Use them for data that the user should be aware of, such as a search category, a customer name, or a content tag. Both attributes and context items are saved in the conversation history. ##### Message attributes Use the `attributes` parameter of [sendMessage()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-sendMessage) to attach metadata to a message. Attributes are forwarded to all connected MCP servers automatically. ``` const aiChatController = editor.plugins.get( 'AIChatController' ); await aiChatController.sendMessage( { message: 'Find related documents about this project', attributes: { department: 'engineering', projectId: 'proj-123' } } ); ``` ##### Tool context items (advanced) Use [addToolItemToContext()](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContext.html#function-addToolItemToContext) to add context items that are displayed as pills in the chat UI and routed to a specific MCP server or tool. This is useful for scoping information like a search category, customer name, or content tag that the user should see and can interact with. ``` const aiChatController = editor.plugins.get( 'AIChatController' ); const conversation = aiChatController.activeConversation; conversation.context.addToolItemToContext( { type: 'mcp-tool-context', mcpServerName: 'my-server', toolName: 'search-docs', // Optional. Omit to send context to all tools on the server. data: { category: 'compliance', region: 'EMEA' }, id: 'search-scope-1' // Optional, 1-21 characters. Auto-generated if omitted. } ); ``` ### Common API The following APIs are used when integrating MCP tools with the editor: * [AIChatController#registerToolDataCallback()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-registerToolDataCallback) – registers a callback to handle data from MCP tools (results and notifications). * [AIChatController#sendMessage()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-sendMessage) – sends a message programmatically, with optional `attributes` forwarded to MCP servers. * [AIChatContext#addToolItemToContext()](../../api/module%5Fai%5Faichat%5Fmodel%5Faichatcontext-AIChatContext.html#function-addToolItemToContext) – adds a context item targeted at a specific MCP server or tool. * [AIChatFeedAPI](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatFeedAPI.html) – methods available inside tool data callbacks for manipulating the chat feed (`insertTextReply`, `insertCustomElement`, `setLoadingMessage`, `clearLoadingMessage`). * [AIToolData](../../api/module%5Fai%5Faicore%5Fmodel%5Faiinteraction-AIToolData.html) – the data object passed to tool data callbacks, containing `toolName`, `type`, `data`, and `attributes`. ### Related resources * [Deployment options](#ckeditor5/latest/features/ai/ckeditor-ai-deployment.html) – learn about Cloud and On-premises deployment modes. * [AI Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) – learn about the AI Chat feature. * [Integration guide](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html) – learn how to set up CKEditor AI in your application. * [On-premises documentation](#cs/latest/onpremises/ckeditor-ai-onpremises/overview.html) – server-side setup and configuration. * [REST API documentation](https://ai.cke-cs.com/v1/docs) – API reference for the AI service. source file: "ckeditor5/latest/features/ai/ckeditor-ai-overview.html" ## CKEditor AI By integrating AI writing assistance directly into the editing experience, CKEditor AI empowers authors with real-time AI writing support, streamlines content creation, and enhances editorial workflows across a wide range of use cases – from productivity boosts and proof-reading to content quality and consistency. If you wish to test CKEditor AI, the access to it is enabled on [the free trial](https://portal.ckeditor.com/signup?callbackUrl=/checkout?plan%3Dfree). ### Demo * **AI Chat** – Use the AI Chat in the side panel to create and edit content on the go with natural language. For example, ask in the chat to add emojis to headings in the content. * **AI Chat History** – Check history for past conversations in the document. * **AI Quick Actions** – Select content and use the balloon toolbar dropdown for Quick Actions and choose from predefined commands. * **AI Review** – Run the review with the improve clarity command. * **AI Translate** – Translate content to various languages with AI-powered translation suggestions. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Why choose CKEditor AI? CKEditor AI is an AI-powered writing assistant that integrates directly into our rich-text editor, CKEditor 5, providing instant text rewriting, summarization, correction, and contextual chat help based on internal style guides. The platform includes automated review tools and enterprise-ready functionality that plugs into existing systems without requiring custom infrastructure. Teams can implement a full suite of AI writing tools in weeks rather than months, delivering streamlined, compliant content workflows that maintain brand consistency and integrate seamlessly with existing document management systems. The core components of CKEditor AI are: * CKEditor 5: A modern rich text editor with dozens of features that improve writing workflows, including collaboration. * AI Service: A state-of-the-art backend AI engine that incorporates multiple models and delivers high-quality content. Available as a [Cloud (SaaS) or on-premises deployment](#ckeditor5/latest/features/ai/ckeditor-ai-deployment.html). The AI Service also provides a REST API. ### CKEditor AI features There are four main features of CKEditor AI. Each feature is a standalone plugin that can be enabled or disabled independently, so you can tailor the AI experience to your needs. Refer to the [integration guide](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--enabling-individual-features) to learn how to configure them selectively. You can test them all using [the free trial](https://portal.ckeditor.com/signup?callbackUrl=/checkout?plan%3Dfree). #### AI Chat The [CKEditor AI Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) is a conversational AI that can be used to aid content creation and editing. It introduces a dynamic chat interface designed to facilitate rich, multi-turn interactions between users and an AI Assistant. This capability moves beyond single-prompt content generation, enabling a more interactive and collaborative experience within writing workflows. It also provides context setting and model selection to better suit the needs of specific content and holds chat history for quick reference of previous work. The Chat is also capable of using the web for more up-to-date information and reasoning to think more deeply about the answers and changes it is allowed to make. #### AI Quick actions [Quick actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html) streamline routine content transformations by offering one-click AI-powered suggestions directly within the editor. You can also ask questions about your selected text in the Chat to get instant AI insights and analysis. This feature enhances speed, relevance, and usability, particularly for repeatable or simple tasks, while preserving deeper chat-based functionality when needed. #### AI Review The [Review](#ckeditor5/latest/features/ai/ckeditor-ai-review.html) feature provides users with AI-powered quality assurance for their content by running checks for grammar, style, tone, and more. It also introduces an intuitive interface for reviewing and managing AI-suggested edits directly within the document, ensuring content meets professional standards with minimal manual effort. #### AI Translate The [AI Translate](#ckeditor5/latest/features/ai/ckeditor-ai-translate.html) feature provides users with AI-powered translations. It introduces an intuitive interface for reviewing and managing AI-suggested translations directly within the document, ensuring content is translated with minimal manual effort. ### Permissions Developers can control access to AI features, models, and capabilities based on user roles, subscription tiers, and organizational requirements. [Learn more about the permissions system](#cs/latest/guides/ckeditor-ai/permissions.html). ### Privacy and data handling You can find detailed information on how CKEditor AI manages your data in [Cloud Services documentation](#cs/latest/guides/ckeditor-ai/overview.html--data-handling-and-security). ### Programmatic usage CKEditor AI features can also be controlled entirely from code – useful for building custom UI, automating workflows, or integrating AI capabilities into your application logic beyond the built-in editor toolbar. The programmatic usage covers two approaches: * **Front-end editor API** – Trigger AI Chat messages and Quick Actions directly from the editor instance. * **REST API** – Call the AI service from your frontend to build AI-powered features around the editor. See the [programmatic usage guide](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html) for details, examples, and live demos. ### Known issues and caveats #### General HTML Support CKEditor AI may not work correctly when [General HTML Support](#ckeditor5/latest/features/html/general-html-support.html) for block elements is enabled. This issue will be addressed in future updates. In the meantime, we recommend avoiding configurations that may cause problems. Learn more about the [configuration of the General HTML Support feature](#ckeditor5/latest/features/html/general-html-support.html--configuration). #### Issues with tables We are aware of certain glitches that may occur when the AI modifies complex [tables](#ckeditor5/latest/features/tables/tables.html) or [layout tables](#ckeditor5/latest/features/tables/layout-tables.html). To prevent data loss, please ensure that the content around these structures remains intact when using CKEditor AI tools, while our team investigates the causes and potential solutions to this issue. #### Editor context and multiple editor handling While it’s possible to use CKEditor AI with multiple editors in an [editor context](#ckeditor5/latest/features/collaboration/context-and-collaboration-features.html), only the first editor registered in the context will currently be able to interact with AI tools and benefit from the content suggestions made by the AI. As our team works on resolving this issue, we recommend using standalone editor instances with CKEditor AI. #### Inline image processing issues [Inline images](#ckeditor5/latest/features/images/images-overview.html) may not be processed correctly by commands within the AI Review feature. A solution is currently under development. #### No Markdown output support CKEditor AI does not currently support output in [Markdown](#ckeditor5/latest/features/markdown.html) format. The AI response pipeline relies on HTML-based collaboration markers and suggestion highlights to power features like [Track Changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) and [Comments](#ckeditor5/latest/features/collaboration/comments/comments.html). These markers have no equivalent in Markdown, making HTML the only supported output format for now. #### Limited interactivity in Chat history The interactivity of [historical AI Chat conversations](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--chat-history) can become limited over time. #### Issues with Gemini 3 Pro model The Gemini 3 Pro model may occasionally return empty responses. If this occurs, we recommend sending a follow-up message asking the Assistant to complete the previous request, or starting a new conversation. ### Common API #### UI components The AI features register the following UI components: | [Component](#ckeditor5/latest/getting-started/setup/toolbar.html) name | Registered by | | The 'toggleAi' button | [AIEditorIntegration](../../api/module%5Fai%5Faieditorintegration%5Faieditorintegration-AIEditorIntegration.html) | | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | --------------------- | ----------------------------------------------------------------------------------------------------------------- | | The 'aiQuickActions' dropdown | [AIQuickActions](../../api/module%5Fai%5Faiquickactions%5Faiquickactions-AIQuickActions.html) | | | | | The 'ask-ai' button | | | | | | The 'explain' button | | | | | | The 'summarize' button | | | | | | The 'highlight-key-points' button | | | | | | The 'improve-writing' button | | | | | | The 'continue' button | | | | | | The 'fix-grammar' button | | | | | | The 'make-shorter' button | | | | | | The 'make-longer' button | | | | | | The 'make-tone-casual' button | | | | | | The 'make-tone-direct' button | | | | | | The 'make-tone-friendly' button | | | | | | The 'make-tone-confident' button | | | | | | The 'make-tone-professional' button | | | | | | The 'translate-to-english' button | | | | | | The 'translate-to-chinese' button | | | | | | The 'translate-to-french' button | | | | | | The 'translate-to-german' button | | | | | | The 'translate-to-italian' button | | | | | | The 'translate-to-portuguese' button | | | | | | The 'translate-to-russian' button | | | | | #### Editor commands | [Component](#ckeditor5/latest/getting-started/setup/toolbar.html) name | Command class | Belongs to (top–level plugin) | | The 'toggleAi' command | [ToggleAICommand](../../api/module%5Fai%5Faieditorintegration%5Faitogglebutton%5Ftoggleaicommand-ToggleAICommand.html) | [AIEditorIntegration](../../api/module%5Fai%5Faieditorintegration%5Faieditorintegration-AIEditorIntegration.html) | | ---------------------------------------------------------------------- | ------------- | ----------------------------- | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ### Future ideas - share your feedback! Have an idea for future improvements? We’d love to hear from you! Share your thoughts and suggestions with us through our [contact form](https://ckeditor.com/contact/). source file: "ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html" ## Using CKEditor AI programmatically CKEditor AI features are designed to work seamlessly through the built-in UI – but they can also be controlled entirely from code. This page covers two distinct approaches to using AI programmatically: the front-end editor API and the REST API. ### Architecture overview Out of the box, CKEditor 5 communicates with the CKEditor AI Service automatically – AI Chat, Quick Actions, Review, and Translate work without any extra setup. The diagram below shows how this fits into your application and what you can build around it. Beyond the built-in features, you can extend AI capabilities in several ways: * **Custom frontend logic:** call the AI service directly using the [REST API](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html--rest-api) or the [editor’s API](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html--front-end-editor-api) to build features outside the editor UI, such as generating titles, summaries, or metadata. * **Custom backend logic:** automate AI workflows server-side via the [REST API](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html--rest-api) for bulk processing, content pipelines, or scheduled tasks. * **[MCP servers](#ckeditor5/latest/features/ai/ckeditor-ai-mcp.html):** connect external tools and data sources to the AI service via the Model Context Protocol (on-premises deployments only). [Contact us](https://ckeditor.com/contact/) to learn more. * **Internal AI platform:** connect your AI platform with custom features to the CKEditor AI service backend (on-premises only, available on demand). [Contact us](https://ckeditor.com/contact/) to learn more. ### Front-end editor API CKEditor AI features can be triggered programmatically via the editor instance. This is useful for building custom UI, automating workflows, or integrating AI capabilities into your application logic beyond the built-in editor toolbar. All examples below assume the editor is already set up with AI features enabled. See the [integration guide](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html) for setup instructions. #### Chat The [AI Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) feature can be controlled via the `AIChat` plugin. See the [chat documentation](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) for more details on the feature. The demo below shows a generic sales offer for server infrastructure. Select a target company, then click the button to send a personalized rewrite request to AI Chat – all from code, without any user interaction in the chat UI. The prompt includes the company’s profile data so the AI tailors the offer accordingly. This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. ##### Send a message Use the [sendMessage()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-sendMessage) method to programmatically send a message to AI Chat. You can dynamically construct the message based on your application state – for example, including external data like a company profile: ``` const aiChatController = editor.plugins.get( 'AIChatController' ); await aiChatController.sendMessage( { message: `Rewrite this offer for ${ companyName }.\n\nCompany profile:\n${ profileData }` } ); ``` ##### Start a new conversation Use the [startConversation()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-startConversation) method: ``` const aiChatController = editor.plugins.get( 'AIChatController' ); await aiChatController.startConversation(); ``` ##### Add editor selection to chat context Attach the current editor selection as context for the next chat message using [addSelectionToChatContext()](../../api/module%5Fai%5Faichat%5Faichatcontroller-AIChatController.html#function-addSelectionToChatContext): ``` const aiChatController = editor.plugins.get( 'AIChatController' ); aiChatController.addSelectionToChatContext(); ``` #### Quick Actions The [Quick Actions](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html) feature lets you trigger predefined AI actions programmatically via the `AIActions` plugin. See the [quick actions documentation](#ckeditor5/latest/features/ai/ckeditor-ai-actions.html) for the full list of available actions and configuration options. The demo below shows a payment reminder email with hardcoded customer data. The editor is configured with [merge fields](#ckeditor5/latest/features/merge-fields.html) for customer name, amount, due date, and other placeholders. Click the button to run a custom AI action that automatically replaces the hardcoded values with the appropriate merge field placeholders – built from the editor’s merge fields configuration. This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. Quick actions operate on the current editor selection. If the selection is collapsed (no text is selected), the action automatically expands to the nearest block element. ##### Execute an action directly Use the [executeAction()](../../api/module%5Fai%5Faiactions%5Faiactions-AIActions.html#function-executeAction) method to run system actions or fully custom prompts: ``` const aiActions = editor.plugins.get( 'AIActions' ); // Run a system action. await aiActions.executeAction( { actionName: 'improve-writing' }, 'Improve writing' ); // Or run a custom prompt (model is required for custom actions). await aiActions.executeAction( { userMessage: 'Rewrite the selected text as a haiku', model: 'agent-1' }, 'Make it a haiku' ); ``` The available system `actionName` values are defined by the [AIActionsNames](../../api/module%5Fai%5Faiactions%5Faiactions-AIActionsNames.html). ### REST API The same AI service that powers the editor features is also available as a REST API. You can call it from your frontend – using the editor’s authentication token – to build AI-powered features around the editor. This is especially useful for scenarios where you need AI capabilities **outside** the editor content area, such as auto-generating a title or meta description in separate form fields based on the editor content. The demo below shows a form with a title and meta description field above the editor. Click the button to generate both fields from the editor content using the AI REST API. This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. #### What you can do The AI REST API (`https://ai.cke-cs.com`) exposes three categories of endpoints: * **[Actions](#cs/latest/guides/ckeditor-ai/actions.html)** – Stateless, single-purpose content transforms. Use these for operations like fixing grammar, improving writing, translating short sections, adjusting length or tone, or running custom prompts against content. * **[Conversations](#cs/latest/guides/ckeditor-ai/conversations.html)** – Multi-turn chat with conversation history, file uploads, and web search capabilities. * **[Reviews](#cs/latest/guides/ckeditor-ai/reviews.html)** – Document analysis for grammar, clarity, readability, and tone, returning specific suggestions for improvement. Also supports full-document translation, ensuring all text is translated even in longer content. #### Calling the REST API from the frontend AI generation endpoints, such as Actions calls and Conversation message calls, return **Server-Sent Events (SSE)** streams. This means you cannot simply `await response.json()` for these responses – instead, you need to read the response stream and parse the individual events. Other REST API endpoints, such as the models endpoint, return regular JSON responses. The following example shows how to call the AI Actions API from the browser and collect the streamed result: ``` // Get the editor content. const html = editor.getData(); // Get the auth token from the editor's token provider. const token = editor.plugins.get( 'CloudServices' ).token.value; // Call the AI Actions API (system action: improve-writing). const response = await fetch( 'https://ai.cke-cs.com/v1/actions/system/improve-writing/calls', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ token }` }, body: JSON.stringify( { content: [ { type: 'text', content: html } ] } ) } ); // Read the SSE stream. // Each SSE message has an "event:" line (e.g. "text-delta") and a "data:" line with JSON. const reader = response.body.getReader(); const decoder = new TextDecoder(); let result = ''; let currentEvent = ''; while ( true ) { const { done, value } = await reader.read(); if ( done ) { break; } const chunk = decoder.decode( value, { stream: true } ); for ( const line of chunk.split( '\n' ) ) { if ( line.startsWith( 'event: ' ) ) { currentEvent = line.slice( 7 ).trim(); } else if ( line.startsWith( 'data: ' ) && currentEvent === 'text-delta' ) { const data = JSON.parse( line.slice( 6 ) ); result += data.textDelta; } } } ``` #### Full REST API documentation For the complete API reference, including all available endpoints, request and response formats, streaming, and authentication details, see the [AI REST API documentation](https://ai.cke-cs.com/docs). source file: "ckeditor5/latest/features/ai/ckeditor-ai-review.html" ## AI Review The AI Review feature provides users with AI-powered quality assurance for their content by running checks for grammar, style, tone, and more. It also introduces an intuitive interface for reviewing and managing AI-suggested edits directly within the document, ensuring content meets professional standards with minimal manual effort. ### Demo This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. ### Integration To start using the AI Review feature, first load the `AIReviewMode` plugin in your editor configuration. The AI Review button will appear in the AI user interface. [Learn more about installing and enabling AI features](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html). After picking one of the available commands in the AI Review tab, AI will analyze the document and propose a series of suggestions: While in the AI Review, the editor remains read–only and allows you to browse suggestions. You can either click suggestions in the sidebar or select them in the editor content (underlined): You can accept or dismiss review suggestions by clicking the corresponding buttons. You can also accept all suggestions by using the “Accept all” button in the top of the user interface and [preview changes similar to chat suggestions](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html--previewing-changes). Changes that were accepted or dismissed become greyed out in the interface. You can also abandon the review by hitting the “Exit review” button. Once you are done reviewing your document and all changes are accepted or rejected, click “Finish review” (the button state changes automatically) to return to the normal operation of the editor, where typing is possible. ### Review commands The feature comes with several review commands: | Command name | Command description | Additional information | Configuration id | | ------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | **Custom command** | Enter a custom command for a specific review. | You can pick one of the [available AI models](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--supported-ai-models) to execute a custom command. The default model is selected based on the [model configuration](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--configuration). | custom | | **Proofread** | Check the text for errors in grammar, spelling and punctuation. | | correctness | | **Improve clarity** | Improve the logical structure and precision for a clearer message. | | clarity | | **Improve readability** | Adjust sentence structure and word choice for an easier read. | | readability | | **Adjust length** | Shorten or lengthen the text as needed. | _Longer_ and _Shorter_ options available | length | | **Adjust tone and style** | Modify the text to a desired tone and style. | Several tone and style options are available: _Casual, Direct, Friendly, Confident, Professional_ | tone | The list of review commands visible in the AI Review UI can be adjusted by reordering them or removing selected ones. Please refer to the [Available review commands section](#ckeditor5/latest/features/ai/ckeditor-ai-review.html--available-review-commands) below. ### Configuration #### Available review commands The list of available review commands visible in the AI Review UI can be adjusted via `config.ai.review.availableCommands`. This configuration option accepts a list of command IDs. The order of IDs on the list defines the order of commands in the UI. You can limit the list of commands to only desired ones and reorder them at the same time: ``` ClassicEditor .create( { /* ... */ plugins: [ AIReviewMode, AIEditorIntegration, /* ... */ ], ai: { review: { availableCommands: [ 'length', 'tone', 'readability', ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Please refer to the [Review commands section](#ckeditor5/latest/features/ai/ckeditor-ai-review.html--review-commands) above to see all available commands. #### Available models for custom command The AI Review feature uses the unified [model configuration](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--configuration) from `config.ai.models`, which is shared across all AI features ([Chat](#ckeditor5/latest/features/ai/ckeditor-ai-chat.html) and Review). This ensures consistent model selection behavior across your AI features. When using the Custom command, the default model is automatically selected based on the `defaultModelId` configuration. You can also manually select a different model from the available models list, which is filtered by the `displayedModels` configuration. The model selector UI visibility is controlled by the [config.ai.models.showModelSelector](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html--configuration) setting. #### Adding extra commands The AI Review feature supports adding custom commands tailored to your specific needs. Each custom command requires a custom prompt definition. You can optionally specify a model ID to use a particular model instead of the default one. The command definition needs to be provided via `config.ai.review.extraCommands` configuration option. This option registers the command in the AI Review feature. ``` ClassicEditor .create( { /* ... */ plugins: [ AIReviewMode, AIEditorIntegration, /* ... */ ], ai: { review: { extraCommands: [ { id: 'improve-captions', label: 'Improve Captions', description: 'Improve image captions in the document.', prompt: 'Suggest improvements for the image captions in the document.', }, { id: 'expand-abbreviations', label: 'Expand Abbreviations', description: 'Expand abbreviations in the document.', prompt: 'Suggest expansions for abbreviations in the document.', model: 'gpt-5-2' } ] } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` If [config.ai.review.availableCommands configuration option](#ckeditor5/latest/features/ai/ckeditor-ai-review.html--available-review-commands) is not set, the extra commands will be added to the end of the list of available commands and visible in the UI by default. However, once `config.ai.review.availableCommands` is defined, it overrides the default list. In this case, you must explicitly include all commands that should be available in the UI (both default and extra ones), as only the commands listed in this configuration will be exposed: ``` ClassicEditor .create( { /* ... */ plugins: [ AIReviewMode, AIEditorIntegration, /* ... */ ], ai: { review: { availableCommands: [ 'improve-captions', 'expand-abbreviations', 'custom', 'correctness', 'clarity', 'readability', 'length', 'tone', ], extraCommands: [ /* ... */ ], } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Common API #### REST API Document reviews are also available via the [Reviews REST API](#cs/latest/guides/ckeditor-ai/reviews.html). Use this to analyze documents for grammar, clarity, readability, and tone outside the editor, returning specific suggestions for improvement. See the [programmatic documentation](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html) for examples and the [full API reference](https://ai.cke-cs.com). source file: "ckeditor5/latest/features/ai/ckeditor-ai-translate.html" ## AI Translate The AI Translate feature provides users with AI-powered translations. It introduces an intuitive interface for reviewing and managing AI-suggested translations directly within the document, ensuring content meets professional standards with minimal manual effort. ### Demo This demo presents a limited set of AI features. Visit the [CKEditor AI overview](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html--demo) to see more in action. ### Integration To start using the Translate feature, first load the `AITranslate` plugin in your editor configuration. The translate button will appear in the AI user interface. [Learn more about installing and enabling AI features](#ckeditor5/latest/features/ai/ckeditor-ai-integration.html). After choosing one of the available languages in the Translate tab, AI will analyze the document and propose a translation. While in the Translate Mode, the editor remains read-only and allows you to browse translation suggestions. Translation suggestions can be seen in the editor content, visualizing how a fully translated document will look: While the Translate tab shows the original content for better context: You can either click suggestions in the sidebar or select them in the editor content (underlined) to see which translation applies to which part of the original content: You can accept translations by using the “Accept translation” button at the top of the user interface. You can also abandon the review by hitting the “Cancel” button. ### Languages list The default list of languages includes several languages out-of-the-box: _English, Spanish, French, German, Chinese (Simplified), Japanese, Russian, Portuguese, Korean, and Italian_. The list can be [fully customized](#ckeditor5/latest/features/ai/ckeditor-ai-translate.html--customizing-list-of-languages). #### Customizing list of languages The [config.ai.translate.languages](../../api/module%5Fai%5Faitranslate%5Faitranslate-AITranslateConfig.html#member-languages) property allows you to provide a custom list of languages for the “Translate” feature. For instance, the following configuration will replace the default list with “German” and “French”: ``` ClassicEditor .create( { ai: { translate: { languages: [ { id: 'german', label: 'German' }, { id: 'french', label: 'French' } ] } } } ) .then( ... ) .catch( ... ); ``` #### Supported languages The AI Translate feature supports multiple languages with varying quality levels. The production-ready languages (_English, Spanish, French, German, Chinese (Simplified), Japanese, Russian, Portuguese, Korean, Italian_) offer the most reliable results for any-to-any translation pairs. Extended support is available for 40+ additional languages based on LLM provider capabilities, though quality may vary depending on the language pair and content complexity. Translation quality is generally highest for _English-to-X_ or _X-to-English_ pairs, while any-to-any translation between non-English languages outside the production tier may produce less reliable results, particularly with specialized vocabulary. [Contact us](https://ckeditor.com/contact/) if you need more specific translation capabilities. ### Common API #### REST API Translation is available via the REST API through two endpoints, depending on the scope of the content: * **[Actions](#cs/latest/guides/ckeditor-ai/actions.html)** – Best for translating short text sections. The `translate` action performs a single-pass translation of the provided content. * **[Reviews](#cs/latest/guides/ckeditor-ai/reviews.html)** – Best for full documents. The review-based translation ensures all text is translated, including parts that a single-pass action might miss in longer content. See the [programmatic documentation](#ckeditor5/latest/features/ai/ckeditor-ai-programmatic.html) for examples and the [full API reference](https://ai.cke-cs.com). source file: "ckeditor5/latest/features/autoformat.html" ## Autoformatting The autoformat feature lets you quickly format your content with Markdown-like shortcodes. This way you do not need to use toolbar buttons or dropdowns for the most common formatting features. ### Demo Test the autoformatting feature in the editor below. Try using Markdown shortcodes while typing. For example: 1. Start a new line. 2. Press # and then Space. The line will automatically turn into a heading. If needed, you can revert the automatic change by pressing Backspace. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Block formatting The following block formatting options are available: * [Bulleted list](#ckeditor5/latest/features/lists/lists.html) – Start a line with `*` or `-` followed by a space. * [Numbered list](#ckeditor5/latest/features/lists/lists.html) – Start a line with `1.` or `1)` followed by a space. * [To-do list](#ckeditor5/latest/features/lists/todo-lists.html) – Start a line with `[ ]` or `[x]` followed by a space to insert an unchecked or checked list item, respectively. * [Headings](#ckeditor5/latest/features/headings.html) – Start a line with `#` or `##` or `###` followed by a space to create a heading 1, heading 2, or heading 3 (up to heading 6 if [options](../api/module%5Fheading%5Fheadingconfig-HeadingConfig.html#member-options) defines more headings). * [Block quote](#ckeditor5/latest/features/block-quote.html) – Start a line with `>` followed by a space. * [Code block](#ckeditor5/latest/features/code-blocks.html) – Start a line with `` `backticks` ``. * [Horizontal line](#ckeditor5/latest/features/horizontal-line.html) – Start a line with `---`. ### Inline formatting The following [basic styles](#ckeditor5/latest/features/basic-styles.html) inline formatting options are available: * Bold – Type `**text**` or `__text__`, * Italic – Type `*text*` or `_text_`, * Code – Type `` `text` ``, * Strikethrough – Type `~~text~~`. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Creating custom autoformatters The [Autoformat](../api/module%5Fautoformat%5Fautoformat-Autoformat.html) feature bases on [blockAutoformatEditing](../api/module%5Fautoformat%5Fblockautoformatediting.html#function-blockAutoformatEditing) and [inlineAutoformatEditing](../api/module%5Fautoformat%5Finlineautoformatediting.html#function-inlineAutoformatEditing) tools to create the autoformatters mentioned above. You can use these tools to create your own autoformatters. Check the [Autoformat feature’s code](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-autoformat/src/autoformat.ts) as an example. ### Known issues While the autoformatting feature is stable and ready to use, some issues were reported for it. Feel free to upvote 👍 them on GitHub if they are important for you: * Pasting Markdown-formatted content does not automatically convert the pasted syntax markers into properly formatted content. GitHub issues: [#2321](https://github.com/ckeditor/ckeditor5/issues/2321), [#2322](https://github.com/ckeditor/ckeditor5/issues/2322). * Setting a specific code block language is not supported yet (it defaults to plain text on insertion). GitHub issue: [#8598](https://github.com/ckeditor/ckeditor5/issues/8598). ### Related features In addition to enabling automatic text formatting, you may want to check the following productivity features: * [Automatic text transformation](#ckeditor5/latest/features/text-transformation.html) – Enables automatic turning of snippets such as `(tm)` into `™` and `"foo"` into `“foo”`. * [Autolink](#ckeditor5/latest/features/link.html--autolink-feature) – Turns the links and email addresses typed or pasted into the editor into active URLs. * [Mentions](#ckeditor5/latest/features/mentions.html) – Brings support for smart autocompletion. * [Slash commands](#ckeditor5/latest/features/slash-commands.html) – Allows to execute a predefined command by writing its name or alias directly in the editor. * [Markdown output](#ckeditor5/latest/features/markdown.html) – Lets the user output the content as Markdown instead of HTML and [use CKEditor 5 as a WYSIWYG Markdown editor](https://ckeditor.com/blog/CKEditor-5-the-best-open-source-Markdown-editor/). * [Source editing](#ckeditor5/latest/features/source-editing/source-editing.html--markdown-source-view) – Allows for Markdown source edition if configured accordingly. Coupled with the [Markdown output](#ckeditor5/latest/features/markdown.html) feature, the autoformatting feature allows for the full-fledged Markdown WYSIWYG editing experience, as described in the [“CKEditor 5: the best open source Markdown editor”](https://ckeditor.com/blog/CKEditor-5-the-best-open-source-Markdown-editor/) blog post. Visit the [free online Markdown editor](https://onlinemarkdowneditor.dev/) to see this solution implemented. ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/autosave.html" ## Autosave The autosave feature allows you to automatically save the data (for example, send it to the server) when needed. This can happen, for example, when the user changes the content. ### Demo Type some text in the demo below to try out the autosave feature. Try adding rich content such as images or tables, and observe the feature’s behavior. Demo elements and mechanisms are explained below the editor. How to understand this demo: * The status indicator shows if the editor has some unsaved content or pending actions. * If you drop a big image into this editor, you will see that it is busy during the entire period when the image is being uploaded. * The editor is also busy when saving the content is in progress (the `save()`'s promise was not resolved). * The autosave feature has a throttling mechanism that groups frequent changes (like typing) into batches. * The autosave itself does not check whether the data has actually changed. It bases on changes in the [model](#ckeditor5/latest/framework/architecture/editing-engine.html--model) that sometimes may not be “visible” in the data. You can add such a check yourself if you would like to avoid sending the same data to the server twice. * You will be asked whether you want to leave the page if an image is being uploaded or the data has not been saved successfully yet. You can test that by dropping a big image into the editor or changing the “HTTP server lag” to a high value (for example, 9000ms) and typing something. These actions will make the editor busy for a longer time – try leaving the page then. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list. Assuming that you have implemented some form of the `saveData()` function that sends the data to your server and returns a promise which is resolved once the data is successfully saved, configuring the [Autosave](../api/module%5Fautosave%5Fautosave-Autosave.html) feature is simple: The autosave feature listens to the [editor.model.document#change:data](../api/module%5Fengine%5Fmodel%5Fdocument-ModelDocument.html#event-change:data) event, throttles it, and executes the [config.autosave.save()](../api/module%5Fautosave%5Fautosave-AutosaveConfig.html#member-save) function. It also listens to the native [window#beforeunload](https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload) event and blocks it in the following cases: * The data has not been saved yet (the `save()` function did not resolve its promise or it has not been called yet due to throttling). * Or any of the editor features registered a [“pending action”](../api/module%5Fcore%5Fpendingactions-PendingActions.html) (for example, that an image is being uploaded). This automatically secures you from the user leaving the page before the content is saved or some ongoing actions like image upload did not finish. ### Configuration You can configure the minimum period between two save actions using the [config.waitingTime](../api/module%5Fautosave%5Fautosave-AutosaveConfig.html#member-waitingTime) property. This helps to avoid overloading the backend. One second is the default waiting time before the next save action if nothing has changed after the editor data was last saved. ``` ClassicEditor .create( { // ... Other configuration options ... autosave: { waitingTime: 5000, // in ms save( editor ) {} }, } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Demo code The demo example at the beginning of this guide shows a simple integration of the editor with a fake HTTP server (which needs 1000ms to save the content). Here is the demo code: ``` ClassicEditor .create( { attachTo: document.querySelector( '#editor' ), // ... Other configuration options ... autosave: { save( editor ) { return saveData( editor.getData() ); } } } ) .then( editor => { window.editor = editor; displayStatus( editor ); } ) .catch( err => { console.error( err.stack ); } ); // Save the data to a fake HTTP server (emulated here with a setTimeout()). function saveData( data ) { return new Promise( resolve => { setTimeout( () => { console.log( 'Saved', data ); resolve(); }, HTTP_SERVER_LAG ); } ); } // Update the "Status: Saving..." information. function displayStatus( editor ) { const pendingActions = editor.plugins.get( 'PendingActions' ); const statusIndicator = document.querySelector( '#editor-status' ); pendingActions.on( 'change:hasAny', ( evt, propertyName, newValue ) => { if ( newValue ) { statusIndicator.classList.add( 'busy' ); } else { statusIndicator.classList.remove( 'busy' ); } } ); } ``` ### Related features You can read more about [getting and setting data](#ckeditor5/latest/getting-started/setup/getting-and-setting-data.html) in the Getting started section. ### Common API The [Autosave](../api/module%5Fautosave%5Fautosave-Autosave.html) plugin registers the [AutosaveAdapter](../api/module%5Fautosave%5Fautosave-AutosaveAdapter.html) used to save the data. You can use [AutosaveConfig](../api/module%5Fautosave%5Fautosave-AutosaveConfig.html) to control the behavior of the adapter. ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/basic-styles.html" ## Basic text styles The basic styles feature lets you apply the most essential formatting such as bold, italic, underline, strikethrough, subscript, superscript, and code. Coupled with more [formatting features](#ckeditor5/latest/features/basic-styles.html--related-features), these serve as a base for any WYSIWYG editor toolset. ### Demo You may apply basic formatting options with toolbar buttons. You can also make use of the [autoformatting feature](#ckeditor5/latest/features/autoformat.html) that changes Markdown code to formatted text as you type. Use one of these to format text: * Bold – Use the bold toolbar button or type `**text**` or `__text__`. * Italic – Use the italic toolbar button or type `*text*` or `_text_`. * Code – Use the code toolbar button or type `` `text` ``. * Strikethrough – Use the strikethrough toolbar button or type `~~text~~`. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Available text styles | Style feature | [Command](#ckeditor5/latest/framework/architecture/core-editor-architecture.html--commands) name | [Toolbar](#ckeditor5/latest/getting-started/setup/toolbar.html) component name | Output element | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ---------------------- | | [Bold](../api/module%5Fbasic-styles%5Fbold-Bold.html) | 'bold' | 'bold' | bold | | [Italic](../api/module%5Fbasic-styles%5Fitalic-Italic.html) | 'italic' | 'italic' | italic | | [Underline](../api/module%5Fbasic-styles%5Funderline-Underline.html) | 'underline' | 'underline' | underline | | [Strikethrough](../api/module%5Fbasic-styles%5Fstrikethrough-Strikethrough.html) | 'strikethrough' | 'strikethrough' | strikethrough | | [Code](../api/module%5Fbasic-styles%5Fcode-Code.html) | 'code' | 'code' | code | | [Subscript](../api/module%5Fbasic-styles%5Fsubscript-Subscript.html) | 'subscript' | 'subscript' | subscript | | [Superscript](../api/module%5Fbasic-styles%5Fsuperscript-Superscript.html) | 'superscript' | 'superscript' | superscript | #### Supported input By default, each feature can upcast more than one type of content. Here is the full list of elements supported by each feature, either when pasting from the clipboard, loading data on start, or using the [data API](../api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-setData). | Style feature | Supported input elements | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [Bold](../api/module%5Fbasic-styles%5Fbold-Bold.html) | , , <\* style="font-weight: bold"> (or numeric values that are greater or equal 600) | | [Italic](../api/module%5Fbasic-styles%5Fitalic-Italic.html) | , , <\* style="font-style: italic"> | | [Underline](../api/module%5Fbasic-styles%5Funderline-Underline.html) | , <\* style="text-decoration: underline"> | | [Strikethrough](../api/module%5Fbasic-styles%5Fstrikethrough-Strikethrough.html) | , , , <\* style="text-decoration: line-through"> | | [Code](../api/module%5Fbasic-styles%5Fcode-Code.html) | , <\* style="word-wrap: break-word"> | | [Subscript](../api/module%5Fbasic-styles%5Fsubscript-Subscript.html) | , <\* style="vertical-align: sub"> | | [Superscript](../api/module%5Fbasic-styles%5Fsuperscript-Superscript.html) | , <\* style="vertical-align: super"> | ### Typing around inline code CKEditor 5 allows for typing both at the inner and outer boundaries of code to make editing easier for the users. **To type inside a code element**, move the caret to its (start or end) boundary. As long as the code remains highlighted (by default: less transparent gray), typing and applying formatting will be done within its boundaries: **To type before or after a code element**, move the caret to its boundary, then press the Arrow key (→ or ←) once. The code is no longer highlighted and whatever text you type or formatting you apply will not be enclosed by the code element: ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the plugins which you need to your plugin list. Then, simply configure the toolbar items to make the features available in the user interface. ### Related features Check out also these CKEditor 5 features to gain better control over your content style and format: * [Font styles](#ckeditor5/latest/features/font.html) – Easily and efficiently control the font [family](#ckeditor5/latest/features/font.html--configuring-the-font-family-feature), [size](#ckeditor5/latest/features/font.html--configuring-the-font-size-feature), [text or background color](#ckeditor5/latest/features/font.html--configuring-the-font-color-and-font-background-color-features). * [Styles](#ckeditor5/latest/features/style.html) – Apply pre-configured styles to existing elements in the editor content. * [Text alignment](#ckeditor5/latest/features/text-alignment.html) – Because it does matter whether the content is left, right, centered, or justified. * [Case change](#ckeditor5/latest/features/case-change.html) – Turn a text fragment or block into uppercase, lowercase, or title case. * [Code blocks](#ckeditor5/latest/features/code-blocks.html) – Insert longer, multiline code listings, expanding the inline code style greatly. * [Highlight](#ckeditor5/latest/features/highlight.html) – Mark important words and passages, aiding a review or drawing attention to specific parts of the content. * [Format painter](#ckeditor5/latest/features/format-painter.html) – Easily copy text formatting and apply it in a different place in the edited document. * [Autoformatting](#ckeditor5/latest/features/autoformat.html) – Format the text on the go with Markdown code. * [Remove format](#ckeditor5/latest/features/remove-format.html) – Easily clean basic text formatting. ### Common API Each style feature registers a [command](#ckeditor5/latest/features/basic-styles.html--available-text-styles) which can be executed from code. For example, the following snippet will apply the bold style to the current selection in the editor: ``` editor.execute( 'bold' ); ``` ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/block-quote.html" ## Block quote The block quote feature lets you easily include block quotations or pull quotes in your content. It is also an attractive way to draw the readers’ attention to selected parts of the text. ### Demo Use the block quote toolbar button in the editor below to see the feature in action. You can also type `>` followed by a space before the quotation to format it on the go thanks to the [autoformatting](#ckeditor5/latest/features/autoformat.html) feature. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Nested block quotes Starting from version 27.1.0, CKEditor 5 will properly display a block quote nested in another block quote. This sort of structure is indispensable in email editors or discussion forums. The ability to cite previous messages and preserve a correct quotation structure is often crucial to maintain the flow of communication. Nested block quotes may also prove useful for scientific or academic papers, but articles citing sources and referring to previous writing would often use it, too. Support for nested block quotes is provided as backward compatibility for loading pre-existing content, for example created in CKEditor 4\. Additionally, pasting content with nested block quotes is supported. You can also nest a block quote in another block quote using the [drag and drop](#ckeditor5/latest/features/drag-drop.html) mechanism – just select an existing block quote and drag it into another. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuration #### Disallow nesting block quotes By default, the editor supports inserting a block quote into another block quote. To disallow nesting block quotes, you need to register an additional schema rule. It needs to be added before the data is loaded into the editor, hence it is best to implement it as a plugin: ``` function DisallowNestingBlockQuotes( editor ) { editor.model.schema.addChildCheck( ( context, childDefinition ) => { if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) { return false; } } ); } // Pass it via config.extraPlugins or config.plugins: ClassicEditor .create( { extraPlugins: [ DisallowNestingBlockQuotes ], // The rest of the configuration. // ... } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Related features Here are some other CKEditor 5 features that you can use similarly to the block quote plugin to structure your text better: * [Block indentation](#ckeditor5/latest/features/indent.html) – Set indentation for text blocks such as paragraphs or lists. * [Code block](#ckeditor5/latest/features/code-blocks.html) – Insert longer, multiline code listings. * [Text alignment](#ckeditor5/latest/features/text-alignment.html) – Align your content left, right, center it, or justify. * [Autoformatting](#ckeditor5/latest/features/autoformat.html) – Add formatting elements (such as block quotes) as you type with Markdown code. ### Common API The [BlockQuote](../api/module%5Fblock-quote%5Fblockquote-BlockQuote.html) plugin registers: * the `'blockQuote'` UI button component implemented by the [block quote UI feature](../api/module%5Fblock-quote%5Fblockquoteui-BlockQuoteUI.html), * the `'blockQuote'` command implemented by the [block quote editing feature](../api/module%5Fblock-quote%5Fblockquoteediting-BlockQuoteEditing.html). You can execute the command using the [editor.execute()](../api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-execute) method: ``` // Applies block quote to the selected content. editor.execute( 'blockQuote' ); ``` ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/bookmarks.html" ## Bookmarks The bookmarks feature allows for adding and managing the bookmarks anchors attached to the content of the editor. These provide fast access to important content sections, speed up the editing navigation and contribute to a more efficient content creation. ### Demo Use the bookmark toolbar button in the editor below to see the feature in action. Or use the “Insert” command from the menu bar to add a bookmark. Add a unique name to identify the bookmark (for example, `Rights`). You can change the bookmark’s name or remove it by clicking the bookmark icon inside the content. To use the bookmark as an anchor in the content, add a link and put the bookmark name as target. In the example below it could be `#Rights`. The link insertion panel will display all bookmarks available in the edited content (see the [Integration with the link feature](#ckeditor5/latest/features/bookmarks.html--integration-with-the-link-feature) section below). This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Handling the anchor markup Do not worry about setting a bookmark inside an empty paragraph. The block with the `a` tag will not be rendered in the final content (for example for printing). The feature converts anchors into bookmarks during the [initialization of the editor](#ckeditor5/latest/getting-started/setup/getting-and-setting-data.html--initializing-the-editor-with-data) or while [replacing the editor data with setData()](#ckeditor5/latest/getting-started/setup/getting-and-setting-data.html--replacing-the-editor-data-with-setdata). The notation based on the `id` attribute in an `a` HTML element without a `href` attribute is converted. Similar notations meet the conditions, too: * an `a` HTML element with a `name` attribute, * an `a` HTML element with the same `name` and `id` attributes, * an `a` HTML element with different `name` and `id` attributes. By default, all bookmarks created in the editor only have the `id="..."` attribute in the [editor data](#ckeditor5/latest/getting-started/setup/getting-and-setting-data.html--getting-the-editor-data-with-getdata). ### Integration with the link feature Bookmarks integrate with [links](#ckeditor5/latest/features/link.html), providing a smooth linking experience. If you have any bookmarks in your content, you can access the “Bookmarks” panel available during link creation. It will display all bookmarks available in the edited content. Choose one of the anchors from the list and use it as a link target. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuration By default, the conversion of wrapped anchors is turned on. It allows to convert non-empty anchor elements into bookmarks. For example: ``` Foo bar baz ``` will be converted into a bookmark and the output will look like on the example below: ``` Foo bar baz ``` You can disable the automatic conversion by setting the [config.bookmark.enableNonEmptyAnchorConversion](../api/module%5Fbookmark%5Fbookmarkconfig-BookmarkConfig.html#member-enableNonEmptyAnchorConversion) to `false` in the editor configuration. ``` ClassicEditor .create( { // ... Other configuration options ... bookmark: { enableNonEmptyAnchorConversion: false } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Bookmark toolbar configuration The bookmark UI contains a contextual toolbar that appears when a bookmark is selected. You can configure what items appear in this toolbar using the [config.bookmark.toolbar](../api/module%5Fbookmark%5Fbookmarkconfig-BookmarkConfig.html#member-toolbar) option. The following toolbar items are available: * `'bookmarkPreview'` – Shows the name of the bookmark. * `'editBookmark'` – Opens a form to edit the bookmark name. * `'removeBookmark'` – Removes the bookmark. By default, the bookmark toolbar is configured as follows: ``` ClassicEditor .create( { bookmark: { toolbar: [ 'bookmarkPreview', '|', 'editBookmark', 'removeBookmark' ] } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Custom toolbar items You can extend the bookmark toolbar with custom items by registering them in the [component factory](../api/module%5Fui%5Fcomponentfactory-ComponentFactory.html) and adding them to the toolbar configuration. Here is an example of registering a custom component: ``` class MyCustomPlugin extends Plugin { init() { const editor = this.editor; editor.ui.componentFactory.add( 'myCustomBookmarkInfo', locale => { const button = new ButtonView( locale ); const bookmarkCommand = editor.commands.get( 'insertBookmark' ); button.bind( 'isEnabled' ).to( bookmarkCommand, 'value', href => !!href ); button.bind( 'label' ).to( bookmarkCommand, 'value' ); button.on( 'execute', () => { // Add your custom component logic here } ); return button; } ); } } ``` Once registered, the component can be used in the toolbar configuration: ``` ClassicEditor .create( { plugins: [ MyCustomPlugin, /* ... */ ], bookmark: { toolbar: [ 'myCustomBookmarkInfo', '|', 'editBookmark', 'removeBookmark' ] } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Bookmarks on blocks At this time, if a bookmark is attached to a block, it appears before it. However, we plan to expand this solution in the future. We invite you to help us [gather feedback for linking directly to blocks and auto generating IDs](https://github.com/ckeditor/ckeditor5/issues/17264). ### Related features Here are some other CKEditor 5 features that you can use similarly to the bookmark plugin to cross-link and structure your text better: * The [link feature](#ckeditor5/latest/features/link.html) allows adding local and global URLs to the content. * The [document outline](#ckeditor5/latest/features/document-outline.html) displays the list of sections (headings) of the document next to the editor. * The [table of contents](#ckeditor5/latest/features/document-outline.html) lets you insert a widget with a list of headings (section titles) that reflects the structure of the document. ### Common API The [Bookmark](../api/module%5Fbookmark%5Fbookmark-Bookmark.html) plugin registers the `'bookmark'` UI button component implemented by the [bookmark UI feature](../api/module%5Fbookmark%5Fbookmarkui-BookmarkUI.html), and the following commands: * the `'insertBookmark'` command implemented by the [editing feature](../api/module%5Fbookmark%5Finsertbookmarkcommand-InsertBookmarkCommand.html). * the `'updateBookmark'` command implemented by the [editing feature](../api/module%5Fbookmark%5Fupdatebookmarkcommand-UpdateBookmarkCommand.html). ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/case-change.html" ## Case change The case change feature lets you quickly change the letter case of the selected content. You can use it to format part of the text like a title or change it to all-caps and back. ### Demo The demo below lets you test the case change. Place the cursor inside a block such as a paragraph, heading, or list item to affect the entire block. You can also select a text fragment you want to change. Then, apply the case formatting with the toolbar dropdown. You can also use the Shift+F3 keyboard shortcut to cycle through case formats: UPPERCASE > lowercase > Title Case. Undo changes with Ctrl/Cmd+Z. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: #### Activating the feature To use this premium feature, you need to activate it with proper credentials. Refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. ### Configuring the title case mode Approaches to the title case change vary. This is why we did not add a default ruleset. You can use the [config.caseChange.titleCase](../api/module%5Fcase-change%5Fcasechange-CaseChangeTitleCaseConfig.html) configuration to apply your rules. The configuration allows for adding exclusions – words that the feature should not capitalize, as shown in the example below. It also provides an entry point for writing custom sentence end detection mechanisms to handle exclusions in special positions in the sentence. ``` ClassicEditor .create( { // ... Other configuration options ... caseChange: { titleCase: { excludeWords: [ 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'en', 'for', 'if', 'in', 'nor', 'of', 'on', 'or', 'per', 'the', 'to', 'vs', 'vs.', 'via' ] } }, } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Related features Here are some more CKEditor 5 features that can help you format your content: * [Basic text styles](#ckeditor5/latest/features/basic-styles.html) – The essentials, like **bold**, _italic_ and others. * [Font styles](#ckeditor5/latest/features/font.html) – Control the font [family](#ckeditor5/latest/features/font.html--configuring-the-font-family-feature), [size](#ckeditor5/latest/features/font.html--configuring-the-font-size-feature), and [text or background color](#ckeditor5/latest/features/font.html--configuring-the-font-color-and-font-background-color-features). * [Autoformatting](#ckeditor5/latest/features/autoformat.html) – Format the text on the go using Markdown. * [Remove format](#ckeditor5/latest/features/remove-format.html) – Easily clean basic text formatting. ### Common API The [CaseChange](../api/module%5Fcase-change%5Fcasechange-CaseChange.html) plugin registers: * The `'caseChange'` UI dropdown component. * The `'changeCaseUpper'`, `'changeCaseLower'`, and `'changeCaseTitle'` commands implemented by [CaseChangeCommand](../api/module%5Fcase-change%5Fcasechangecommand-CaseChangeCommand.html). You can execute the command using the [editor.execute()](../api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-execute) method: ``` // Change the case of selected content to uppercase. editor.execute( 'changeCaseUpper' ); // Change the case of selected content to lowercase. editor.execute( 'changeCaseLower' ); // Change the case of selected content to title case. editor.execute( 'changeCaseTitle' ); ``` source file: "ckeditor5/latest/features/cloud-services/cloud-services-overview.html" ## Cloud Services Overview The CKEditor Cloud Services is a cloud platform that provides editing and real-time collaboration services. These services for managing comments, storing document revisions, handling the users, synchronizing the data, utilizing, configuring and managing features, importing and exporting documents and managing assets. The platform primarily focuses on providing a backend for the CKEditor 5 features (via dedicated plugins), although some features can also be used directly through [RESTful APIs and hooks](#ckeditor5/latest/features/cloud-services/cloud-services-rest-api.html). ### Real-time collaboration CKEditor Cloud Services offers a fast and highly scalable service for real-time collaboration, compatible with rich text editors built on top of the CKEditor 5 Framework. It is capable of handling real-time collaboration on text documents and tracking users connected to the document. It also serves as a storage for comments, suggestions, and revisions added to the document. Apart from having a supporting backend to transmit operations, resolve conflicts, and apply changes between users connected to the same document, some features are needed on the client side to offer a full real-time collaboration experience: * Showing multiple cursors and selections coming from other users. * Showing users connecting to and disconnecting from the document. * Offering the UI for managing comments and markers in the document. The CKEditor Ecosystem offers a collection of plugins that can be integrated with any CKEditor 5 build to provide a fully flexible and customizable experience. #### CKEditor 5 Real-time collaboration features [CKEditor 5 Real-time collaboration features](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) let you customize any CKEditor 5 integration to include real-time collaborative editing, commenting, and track changes features and tailor them to your needs. Real-time collaboration consists of four features delivered as separate plugins that can be used with any CKEditor 5 build: * [Real-time collaborative editing](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) – Allows for editing the same document by multiple users simultaneously. It also automatically solves all conflicts if users make changes at the same time. * [Real-time collaborative comments](#ckeditor5/latest/features/collaboration/comments/comments.html) – Makes it possible to add comments to any part of the content in the editor. * [Real-time collaborative track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) – Changes to the content are saved as suggestions that can be accepted or discarded later. * [Real-time collaborative revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history.html) – Multiple versions of the document are available, an older version can be restored. * [Users selection and presence list](#ckeditor5/latest/features/collaboration/users.html) – Shows the selection of other users and lets you view the list of users currently editing the content in the editor. All of the above features are customizable. This makes implementing real-time collaborative editing within your application a highly customizable out-of-the-box experience. For an introduction to CKEditor 5 Collaboration Features refer to the [Collaboration overview](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html). ### Export features CKEditor Cloud Services offers a fast and highly scalable service enabling the user to export documents either to a Microsoft Word document or to a PDF document. Both of these are available as a service, making it possible to feed the data straight into the Cloud Services server for more advanced use or as convenient WYSIWYG editor plugins for ease of use in less demanding cases. #### Export to PDF The Export to PDF converter provides an API for converting HTML documents to PDF files. The service generates a file and returns it to the user so they can save it in the `.pdf` format on their disk. This allows you to easily turn your content into the portable final PDF format file collection. Available both [as a service endpoint](#cs/latest/guides/export-to-pdf/overview.html) (a premium feature) and [as a plugin](#ckeditor5/latest/features/converters/export-pdf.html) (needs to be added to the editor separately). #### Export to Word The Export to Word converter provides an API for converting HTML documents to Microsoft Word `.docx` files. The service generates a Word file and returns it to the user so they can save it in the `.docx` format on their disk. This allows you to easily export your content to the Microsoft Word format. Available both [as a service endpoint](#cs/latest/guides/export-to-word/overview.html) (a premium feature) and [as a plugin](#ckeditor5/latest/features/converters/export-word.html) (needs to be added to the editor separately). ### Import from Word The Import from Word converter is a fast and highly scalable service enabling the user to import documents from a Microsoft Word `.docx` file. The feature is available as a service, making it possible to send a `.docx` file [straight into the Cloud Services server](#cs/latest/guides/import-from-word/overview.html) for more advanced use or as a convenient [CKEditor 5 WYSIWYG editor plugin](#ckeditor5/latest/features/converters/import-word/import-word.html) for the ease of use in less demanding cases. The DOCX to HTML converter provides an API for converting Microsoft Word `.docx` files to HTML documents. The service generates HTML data and returns it to the user so they can save it in the HTML format on their disk. ### CKBox [CKBox](#ckbox/latest/guides/index.html) is a service that manages document assets and images. It allows for seamless uploading and management of assets within a document. The service stores them in persistent storage and provides tools to optimize image size and manage attributes, such as alternative text. Once an asset is stored, it can be reused in multiple documents. If you plan to use it withing CKEditor 5, refer to the [CKBox plugin documentation](#ckeditor5/latest/features/file-management/ckbox.html) for details. ### Cloud region and on-premises options Cloud Services are available both as a cloud solution (SaaS) and as a self-hosted (on-premises) version. You can learn more about the different approaches in the dedicated [SaaS vs. On-premises](#cs/latest/guides/saas-vs-on-premises.html) guide in the Cloud Services documentation. The cloud solution offers two regions to host your services. You can choose the [EU, US, or both cloud regions](#cs/latest/guides/saas-vs-on-premises.html--cloud-region) when working with a cloud-hosted SaaS setup. ### Next steps * If you already use collaboration features without Real-time Collaboration you can refer to our dedicated guide about [migration of data between asynchronous and RTC editors](#cs/latest/guides/collaboration/migrating-to-rtc.html). * If you need to save your documents in portable file formats, check out the [Export to PDF](#ckeditor5/latest/features/converters/export-pdf.html) or [Export to Word](#ckeditor5/latest/features/converters/export-word.html) feature guides. * If you need to import your documents from the `.docx` format, learn more about the [Import from Word](#ckeditor5/latest/features/converters/import-word/import-word.html) feature. * If you are interested in the CKBox asset manager, check the [CKBox quick start](#ckbox/latest/guides/quick-start.html) guide for a short instruction on how to start using CKBox. source file: "ckeditor5/latest/features/cloud-services/cloud-services-rest-api.html" ## REST API and Webhooks ### Cloud Services RESTful APIs CKEditor Cloud Services offer several REST APIs that can be used for server integration. They provide a lot of powerful methods that make it possible to control and manage data. They can be used to control content revisions, handle users or manage document conversions. The APIs currently include: * **CKEditor Cloud Services RESTful APIs** – Provides a full-featured RESTful API that you can use to create a server-to-server integration. The API documentation is available at . It is an aggregator of all RESTful APIs currently available. * **CKBox RESTful API** – Provides an API for managing data stored in the CKBox. The API documentation is available at . * **HTML to PDF Converter API** – Provides an API for converting HTML/CSS documents to PDF format. The API documentation is available at . * **HTML to DOCX Converter API** – Provides an API for converting HTML documents to Microsoft Word `.docx` files. The API documentation is available at * **DOCX to HTML Converter API** – Provides an API for converting Microsoft Word `.docx`/`.dotx` files to HTML documents. The API documentation is available at . #### Usage Each method can be used for different purposes. For example, the REST API methods for comments allow for synchronizing comments between CKEditor Cloud Services and another system. In addition to that, CKEditor Cloud Services can be used as a database for comments because it is possible to download them via the REST API at the time they are being displayed. An example of using another API method is getting the content of the document from a collaborative editing session. This feature can be used to build an autosave mechanism for the document, which should reduce transfer costs – autosave requests are not executed by each connected user but only by the system once at a time. ### Webhooks Webhooks resemble a notification mechanism that can be used to build integrations with CKEditor Cloud Services. CKEditor Cloud Services sends an HTTP POST request to a configured URL when specified events are triggered. Webhooks can be used for data synchronization between CKEditor Cloud Services and another system or to build a notifications system. For example, thanks to webhooks, the system might notify the users via email about changes made in the document. To learn more about CKEditor Environment webhooks, refer to the [Webhooks guide](#cs/latest/developer-resources/webhooks/overview.html) in the Cloud Services documentation. source file: "ckeditor5/latest/features/cloud-services/server-side-editor-api.html" ## Server-side editor API Server-side Editor API enables deep and complex integration of your application with all document data, enabling you to manipulate content and manage collaborative data such as suggestions, comments, and revision history, and much more, directly from your server-side code (without running editor instance on the client). The server-side editor REST API endpoint allows you to execute any JavaScript code that uses the CKEditor 5 API, that could be executed by a browser, but without a need to open the editor by a human user. Instead, the script is executed on the Cloud Services server. Please note that there are some [security-related limitations](#cs/latest/developer-resources/server-side-editor-api/security.html) for the executed JavaScript code. ### Why use server-side editor API? While CKEditor 5 provides a rich client-side editing experience, there are many scenarios where server-side content processing is essential: * **Automation**: Run content processing tasks as part of your backend workflows. * **Scalability**: Process multiple documents simultaneously without client-side limitations. * **Security**: Process sensitive content in a controlled environment without exposing it to client-side manipulation. * **Performance**: Handle large-scale content operations without impacting the user’s browser. * **Consistency**: Ensure uniform content changes across multiple documents. * **Integration**: Connect with other server-side systems and databases directly. ### Common use cases * **Deep integration**: Build custom features that can manage document content and related document data straight from your application UI, without a need to open the editor. * **Content migration**: Restructure and update references across multiple documents, perfect for website redesigns or content reorganization. * **Shared content blocks**: Automatically update reusable content (like headers, footers, or common sections) across all documents that use it. * **Automated review systems**: Build systems that automatically review and suggest content changes, like grammar checks or style improvements. * **AI-powered editing**: Make automated suggestions while users are actively editing, helping improve content quality. * **Automated publishing**: Prepare and process content for publication, including formatting, metadata updates, and resolving comments. ### API examples Below, you will find several examples of practical server-side API applications. There are far more possibilities available. #### Getting started with server-side editor API This guide explains how to write scripts that can be executed through the Server-side Editor API endpoint. The following sections provide examples of such scripts, each demonstrating a specific use case that can be automated on the server side. For information about setting up and using the endpoint itself, see the complementary [Cloud Services Server-side Editor API](#cs/latest/developer-resources/server-side-editor-api/editor-scripts.html) documentation. #### Working with content ##### Getting editor data The most basic action you can perform is getting the editor data. ``` // Get the editor data. const data = editor.getData(); // The endpoint will respond with the returned data. return data; ``` You can also retrieve the data with specific options and include additional information about the document: ``` // Get the editor data with suggestion highlights visible. const data = editor.getData( { showSuggestionHighlights: true } ); // Get additional document information. const wordCount = editor.plugins.get( 'WordCount' ).getWords(); // Return both the content and metadata. return { content: data, wordCount: wordCount }; ``` This approach allows you to not only retrieve the document content but also process it, extract metadata, or prepare it for specific use cases like exports or integrations with other systems. ##### Using commands Commands provide a high-level API to interact with the editor and change the document content. Most editor features provide a command that you can use to trigger some action on the editor. Here is a simple example. Imagine you need to fix a typo in a company name that is spread across multiple documents. Instead of forcing the user to do it manually, you can do it with a single line of code: ``` // Replace all instances of "Cksource" with "CKSource" in the document. editor.execute( 'replaceAll', 'CKSource', 'Cksource' ); ``` This command will find all instances of “Cksource” in your documents and change them to “CKSource”. This is perfect for making bulk updates in multiple documents. Simply, execute this call for every document you would like to change. Most CKEditor 5 features expose one or multiple commands that can be used to manipulate the editor’s state. To learn what commands are available, visit [Features guides](#ckeditor5/latest/features/index.html), and look for the “Common API” section at the end of each guide, where commands related to that feature are described. ##### Insert HTML content When you have HTML content ready (for example, from another system or a template), you can insert it directly into the editor. This is often simpler than building the content piece by piece using the editor API. ``` // The HTML content we want to add. const html = '

New section

This is a new section inserted into the document using server-side editor API.

'; // Convert HTML to the editor's model. const model = editor.data.parse( html ); // Get the root element and create an insertion position. const root = editor.model.document.getRoot(); const insertPosition = editor.model.createPositionAt( root, 1 ); // Insert the content at the specified position. editor.model.insertContent( model, insertPosition ); ```
##### Using editor model API If you cannot find a command that would perform a specific action on the document, you can use the editor API to apply precise changes. This approach offers the greatest flexibility and should cover any needs you may have. It requires, however, a better understanding of CKEditor internals. For example, consider a scenario where you need to update all links in your document from `/docs/` to `/documents/`. This is a common task when moving content between environments or updating your site structure. ``` // Get the root element and create a range that covers all content. const root = editor.model.document.getRoot(); const range = editor.model.createRangeIn( root ); const items = Array.from( range.getItems() ); editor.model.change( writer => { for ( const item of items ) { let href = item.getAttribute( 'linkHref' ); if ( item.is( 'textProxy' ) && href ) { // Update the link URL. href = href.replace( '/docs/', '/documents/' ); writer.setAttribute( 'linkHref', href, item ); } } } ); ``` This approach is particularly useful when you have to modify the document data in some specific way, and the generic, high-level API cannot cover it. To learn more about working with the editor engine, see the [Editing engine](#ckeditor5/latest/framework/architecture/editing-engine.html) guide. #### Working with comments The [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) feature allows your users to have discussions attached to certain parts of your documents. You can use the comments feature API to implement interactions with comments with no need to open the editor itself. ##### Creating comments You can create new comments using the `addCommentThread` command. By default, this command would create a comment thread on the current selection and create a “draft” comment thread, which might not be what you want in a server-side context. However, you can customize it using two parameters: `ranges` to specify where to place the comment, and `comment` to set its initial content. Here is an example that shows how to automatically add comments to images that are missing the `alt` attribute: ``` const model = editor.model; // Create a range on the whole content. const range = model.createRangeIn( model.document.getRoot() ); editor.model.change( () => { // Go through each item in the editor content. for ( const item of range.getItems() ) { const isImage = item.is( 'element', 'imageBlock' ) || item.is( 'element', 'imageInline' ); // Find images without `alt` attribute if ( isImage && !item.getAttribute( 'alt' ) ) { const commentRange = model.createRangeOn( item ); const firstCommentMessage = 'The alt attribute is missing.'; // Add a comment on the image. editor.execute( 'addCommentThread', { ranges: [ commentRange ], comment: firstCommentMessage } ); } } } ); ``` The above example shows how to automatically review your content and add comments where needed. You could use similar code to build automated content review systems, accessibility checkers, or any other validation workflows. ##### Resolving comments You can use the comments feature API to manage existing comments in your documents. For example, here is a way to resolve all comment threads in a given document: ``` // Get all comment threads from the document. const threads = editor.plugins.get( 'CommentsRepository' ).getCommentThreads(); // Resolve all open comment threads. for ( const thread of threads ) { if ( !thread.isResolved ) { thread.resolve(); } } ``` This code is particularly useful when you need to clean up a document. You might use it to automatically resolve old discussions, prepare documents for publication, or maintain a clean comment history in your content management system. #### Working with track changes You can leverage the [track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) feature API to manage existing content suggestions, retrieve final document data with all suggestions accepted, or implement automated or AI-powered content reviews. ##### Working with suggestions You can use the [track changes data plugin](../../api/module%5Ftrack-changes%5Ftrackchangesdata-TrackChangesData.html) to get the document data with all suggestions either accepted or discarded: ``` // Get the track changes data plugin. const trackChangesData = editor.plugins.get( 'TrackChangesData' ); // Get the document data with all suggestions rejected. // You can also use `trackChangesData.getDataWithAcceptedSuggestions()` to get data with all suggestions accepted. const data = trackChangesData.getDataWithDiscardedSuggestions(); return data; ``` This is particularly useful when you need to show or process the “original” or the “final” document data. While the previous example could be used to get the data, you may also want to permanently accept or discard suggestions. You can do this for all suggestions at once using the following command: ``` // Accept all suggestions in the document. // Use `discardAllSuggestions` command to discard all suggestions instead. editor.execute( 'acceptAllSuggestions' ); ``` This command is especially helpful when finalizing documents or when working with applications where a document is split into multiple CKEditor document instances but is treated as one unit in the application. In such cases, you might, for example, want to offer a button to accept all suggestions across all document parts. For more granular control, you can also manage individual suggestions: ``` // Get the track changes editing plugin. const trackChangesEditing = editor.plugins.get( 'TrackChangesEditing' ); // Get a specific suggestion by its ID. const suggestion = trackChangesEditing.getSuggestion( 'suggestion-id' ); // Accept the suggestion. suggestion.accept(); // Or discard it. // suggestion.discard(); ``` It allows to display and manage suggestions outside the editor, for example in a separate application view where users can see all comments and suggestions and resolve them without going into the editor. ##### Creating new suggestions Track changes is integrated with most editor commands. If you wish to change the document using commands and track these changes, all you need to do is turn on track changes mode. Below is an example that shows a basic text replacement: ``` // Enable track changes to mark our edits as suggestions. editor.execute( 'trackChanges' ); // Make a simple text replacement. editor.execute( 'replaceAll', 'CKSource', 'Cksource' ); ``` The `trackChanges` command ensures that all changes made by other commands are marked as suggestions. Since Track changes feature is integrated with `Model#insertContent()` function, you can easily suggest adding some new content: ``` // Enable track changes for the new content. editor.execute( 'trackChanges' ); // Prepare the new content to be added. const modelFragment = editor.data.parse( 'Hello world!' ); // Add the content as a suggestion at the beginning of the document. const firstElement = editor.model.document.getRoot().getChild( 0 ); const insertPosition = editor.model.createPositionAt( firstElement, 0 ); editor.model.insertContent( modelFragment, insertPosition ); ``` Now, let’s see how to suggest deleting a specified part of the document. To do this, use `Model#deleteContent()` while in track changes mode: ``` // Enable track changes so that deleted content is marked, // instead of being actually removed from the content. editor.execute( 'trackChanges' ); // Get the section we want to mark as deletion suggestion. const firstElement = editor.model.document.getRoot().getChild( 0 ); // `deleteContent()` expects selection-to-remove as its parameter. const deleteRange = editor.model.createRangeIn( firstElement ); const deleteSelection = editor.model.createSelection( deleteRange ); // Track changes is integrated with `deleteContent()`, so the content // will be marked as suggestion, instead of being removed from the document. editor.model.deleteContent( deleteSelection ); ``` You can use `insertContent()` and `deleteContent()` methods in the following scenarios: * Automated suggestions based on external data. * Creating templates that need review before finalization. * Integrating with content management systems to propose changes. * Building custom workflows for content creation and review. ##### Attribute modifications If you wish to create attributes suggestions using the editor model API, you need to specifically tell the track changes features to record these changes. Let’s look at how to correctly make a suggestion to update links URLs: ``` // Get the track changes editing plugin for direct access to suggestion recording. const trackChangesEditing = editor.plugins.get( 'TrackChangesEditing' ); // Get the root element and create a range that covers all content. const root = editor.model.document.getRoot(); const range = editor.model.createRangeIn( root ); const items = Array.from( range.getItems() ); // Process each item in the document. for ( const item of items ) { editor.model.change( writer => { // Use `recordAttributeChanges to ensure the change is properly recorded as a suggestion. trackChangesEditing.recordAttributeChanges( () => { let href = item.getAttribute( 'linkHref' ); // Only process text proxies (parts of text nodes) that have a `linkHref` attribute. if ( item.is( 'textProxy' ) && href ) { // Update the link URL, for example changing '/docs/' to '/documents/'. href = href.replace( '/docs/', '/documents/' ); // Set the new attribute value, which will be recorded as a suggestion. writer.setAttribute( 'linkHref', href, item ); } } ); } ); } ``` ##### Extracting additional suggestion data Track changes feature stores and exposes more data than is saved on the Cloud Services servers. This dynamic data is evaluated by the feature on-the-fly, hence it is not stored. You can use the editor API to get access to that data. All active suggestions have a related annotation (UI “balloon” element, located in the sidebar or displayed above the suggestion). You can, for example, retrieve a suggestion label that is displayed inside a suggestion balloon annotation. Another useful information is content on which the suggestion was made (together with some additional context around it). The following example demonstrates retrieving additional suggestion data: ``` const results = []; const trackChangesUI = editor.plugins.get( 'TrackChangesUI' ); const annotations = editor.plugins.get( 'Annotations' ).collection; // Go through all annotations available in the document. for ( const annotation of annotations ) { // Check if this is a suggestion annotation. // Note, that another annotation type is `'comment'`. // You can process comments annotations to retrieve additional comments data. if ( annotation.type.startsWith( 'suggestion' ) ) { const suggestion = trackChangesUI.getSuggestionForAnnotation( annotation ); // Get the suggestion label. const label = annotation.innerView.description; // Evaluate the content on which the suggestion was made. // First, get all the ranges in the content related to this suggestion. const ranges = []; // Note, that suggestions can be organized into "chains" when they // are next to each other. Get all suggestions adjacent to the processed one. for ( const adjacentSuggestion of suggestion.getAllAdjacentSuggestions() ) { ranges.push( ...adjacentSuggestion.getRanges() ); } let contextHtml = ''; if ( ranges.length ) { const firstRange = ranges[ 0 ]; const lastRange = ranges[ ranges.length - 1 ]; // Find the common ancestor for the whole suggestion context. const commonAncestor = firstRange.start.getCommonAncestor( lastRange.end ); if ( commonAncestor ) { // Stringify the entire common ancestor element as HTML, highlighting suggestions. contextHtml = editor.data.stringify( commonAncestor, { showSuggestionHighlights: true } ); } } results.push( { type: 'suggestion', id: suggestion.id, label, context: contextHtml } ); } } return results; ``` #### Working with revision history Use the [revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history.html) feature API to build more functional integration between your application and the document revisions data. ##### Saving revisions You can use Revision history API to save a new revision directly from your application backend: ``` // Save the current state as a new revision. editor.plugins.get( 'RevisionTracker' ).saveRevision( { name: 'New revision' } ); ``` This can be used on an unchanged document to simply create a document snapshot, or after you performed some changes to save them as a new revision. Revision history API can help you build an automated mechanism that will automatically create revisions in some time intervals, or based on other factors. It can be particularly useful when you need to create checkpoints for your documents to maintain an audit trail of content modifications. ##### Working with revision data In more complex scenarios, you might have a need to work with content coming from various revisions of your document: ``` // Get the revision management tools. const revisionHistory = editor.plugins.get( 'RevisionHistory' ); const revisionTracker = editor.plugins.get( 'RevisionTracker' ); // Get the latest revision from history. const revision = revisionHistory.getRevisions()[ 0 ]; // Get the document content and document roots attributes. const documentData = await revisionTracker.getRevisionDocumentData( revision ); const attributes = await revisionTracker.getRevisionRootsAttributes( revision ); return { documentData, attributes }; ``` This is useful if you need particular revision data for further processing. It will allow you build custom backend features based on revisions, like previewing revisions data outside of editor, exporting a particular revision to PDF, or integrating revisions data with external systems. ### Custom plugins Server-side editor API capabilities could be extended by creating custom plugins. Custom plugins may implement complex logic and maintain reusable functionality across multiple server-side operations. Through the editor instance, you can access custom plugin API in your server-side scripts. This approach will make your code more organized and maintainable. Using a plugin will be necessary if you need to import a class or a function from one of the CKEditor 5 packages to implement your desired functionality. To use custom plugins in server-side executed scripts, simply add them to the editor bundle that you upload to Cloud Services. Then you can access them through the editor instance: ``` // Get your custom plugin instance. const myPlugin = editor.plugins.get( 'MyCustomPlugin' ); // Use the plugin's API. return myPlugin.doSomething(); ``` For more information about creating custom plugins, see the [Plugins architecture](#ckeditor5/latest/framework/architecture/plugins.html) guide and the [Creating a basic plugin](#ckeditor5/latest/framework/tutorials/creating-simple-plugin-timestamp.html) tutorial. ### Error handling If an error occurs while processing the script on the server side, the API will return an error message and include the specific information about the encountered problem in the `data.error` object. Additionally, a `trace_id` is returned, which allows you to look up more detailed information about the specific event on the server. This makes it easier to quickly diagnose and resolve issues based on the provided identifier. source file: "ckeditor5/latest/features/code-blocks.html" ## Code blocks The code block feature lets you insert and edit blocks of pre-formatted code. It is perfect for presenting programming-related content in an attractive, easy-to-read form. ### Demo Use the code block toolbar button and the type dropdown to insert a desired code block. Alternatively, start a line with a backtick, and the [autoformatting feature](#ckeditor5/latest/features/autoformat.html) will format it as a code block. To add a paragraph under a code block, simply press Enter three times. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. Each code block has a [specific programming language assigned](#ckeditor5/latest/features/code-blocks.html--configuring-code-block-languages) (like “Java” or “CSS”; this is configurable) and supports basic editing tools, for instance, [changing the line indentation](#ckeditor5/latest/features/code-blocks.html--changing-line-indentation) using the keyboard. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuring code block languages Each code block can be assigned a programming language. The language of the code block is represented as a CSS class of the `` element, both when editing and in the editor data: ```
window.alert( 'Hello world!' )
``` It is possible to configure which languages are available to the users. You can use the [codeBlock.languages](../api/module%5Fcode-block%5Fcodeblockconfig-CodeBlockConfig.html#member-languages) configuration and define your own languages. For example, the following editor supports only two languages (CSS and HTML): ``` ClassicEditor .create( { // ... Other configuration options ... codeBlock: { languages: [ { language: 'css', label: 'CSS' }, { language: 'html', label: 'HTML' } ] } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` By default, the CSS class of the `` element in the data and editing is generated using the `language` property (prefixed with “language-”). You can customize it by specifying an optional `class` property. You can set **multiple classes** but **only the first one** will be used as defining language class: ``` ClassicEditor .create( { // ... Other configuration options ... codeBlock: { languages: [ // Do not render the CSS class for the plain text code blocks. { language: 'plaintext', label: 'Plain text', class: '' }, // Use the "php-code" class for PHP code blocks. { language: 'php', label: 'PHP', class: 'php-code' }, // Use the "js" class for JavaScript code blocks. // Note that only the first ("js") class will determine the language of the block when loading data. { language: 'javascript', label: 'JavaScript', class: 'js javascript js-code' }, // Python code blocks will have the default "language-python" CSS class. { language: 'python', label: 'Python' } ] } } ) .then( /* ... */ ) .catch( /* ... */ ); ```
#### Integration with code highlighters Although live code block highlighting _**is impossible when editing**_ in CKEditor 5 ([learn more](https://github.com/ckeditor/ckeditor5/issues/436#issuecomment-548399675)), the content can be highlighted when displayed in the frontend (for example, in blog posts or website content). The code language [configuration](../api/module%5Fcode-block%5Fcodeblockconfig-CodeBlockConfig.html#member-languages) helps to integrate with external code highlighters (for example, [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/)). Refer to the documentation of the highlighter of your choice and make sure the CSS classes configured in `codeBlock.languages` correspond with the code syntax autodetection feature of the highlighter. ### Tips and tweaks #### Editing text around code blocks There could be situations when there is no obvious way to set the caret before or after a block of code and type. This can happen when the code block is preceded or followed by a widget (like a table) or when the code block is the first or the last child of the document (or both). * To type **before the code block**: Put the selection at the beginning of the first line of the code block and press Enter. Move the selection to the empty line that has been created and press Enter again. A new paragraph that you can type in will be created before the code block. * To type **after the code block**: Put the selection at the end of the last line of the code block and press Enter three times. A new paragraph that you can type in will be created after the code block. #### Changing line indentation You can change the indentation of the code using keyboard shortcuts and toolbar buttons: * To **increase** indentation: Select the line (or lines) you want to indent. Hit the Tab key or press the “Increase indent” button in the toolbar. * To **decrease** indentation: Select the line (or lines) the indent should decrease. Hit the Shift+Tab keys or press the “Decrease indent” button in the toolbar. #### Preserving line indentation To speed up the editing, when typing in a code block, the indentation of the current line is preserved when you hit Enter and create a new line. If you want to change the indentation of the new line, take a look at [some easy ways to do that](#ckeditor5/latest/features/code-blocks.html--changing-line-indentation). ### Related features Here are some similar CKEditor 5 features that you may find helpful: * [Basic text styles](#ckeditor5/latest/features/basic-styles.html) – Use the `code` formatting for short inline code chunks. * [Block quote](#ckeditor5/latest/features/block-quote.html) – Include block quotations or pull quotes in your rich-text content. * [Block indentation](#ckeditor5/latest/features/indent.html) – Set indentation for text blocks such as paragraphs or lists. * [Autoformatting](#ckeditor5/latest/features/autoformat.html) – Format the content on the go with Markdown code. ### Common API The [CodeBlock](../api/module%5Fcode-block%5Fcodeblock-CodeBlock.html) plugin registers: * The `'codeBlock'` split button with a dropdown allowing to choose the language of the block. * The ['codeBlock'](../api/module%5Fcode-block%5Fcodeblockcommand-CodeBlockCommand.html) command. The command converts selected WYSIWYG editor content into a code block. If no content is selected, it creates a new code block at the place of the selection. You can choose which language the code block is written in when executing the command. The language will be set in the editor model and reflected as a CSS class visible in the editing view and the editor (data) output: ``` editor.execute( 'codeBlock', { language: 'css' } ); ``` When executing the command, you can use languages defined by the [codeBlock.languages](../api/module%5Fcode-block%5Fcodeblockconfig-CodeBlockConfig.html#member-languages) configuration. The default list of languages is as follows: ``` codeBlock.languages: [ { language: 'plaintext', label: 'Plain text' }, // The default language. { language: 'c', label: 'C' }, { language: 'cs', label: 'C#' }, { language: 'cpp', label: 'C++' }, { language: 'css', label: 'CSS' }, { language: 'diff', label: 'Diff' }, { language: 'go', label: 'Go' }, { language: 'html', label: 'HTML' }, { language: 'java', label: 'Java' }, { language: 'javascript', label: 'JavaScript' }, { language: 'php', label: 'PHP' }, { language: 'python', label: 'Python' }, { language: 'ruby', label: 'Ruby' }, { language: 'typescript', label: 'TypeScript' }, { language: 'xml', label: 'XML' } ] ``` **Note**: If you execute the command with a specific `language` when the selection is anchored in a code block, and use the additional `forceValue: true` parameter, it will update the language of this particular block. ``` editor.execute( 'codeBlock', { language: 'java', forceValue: true } ); ``` **Note**: If the selection is already in a code block, executing the command will convert the block back into plain paragraphs. * The ['indentCodeBlock'](../api/module%5Fcode-block%5Findentcodeblockcommand-IndentCodeBlockCommand.html) and ['outdentCodeBlock'](../api/module%5Fcode-block%5Foutdentcodeblockcommand-OutdentCodeBlockCommand.html) commands. Both commands are used by the Tab and Shift+Tab keystrokes as described in the [section about indentation](#ckeditor5/latest/features/code-blocks.html--changing-line-indentation): * The `'indentCodeBlock'` command is enabled when the selection is anchored anywhere in the code block and it allows increasing the indentation of the lines of code. The indentation character (sequence) is configurable using the [codeBlock.indentSequence](../api/module%5Fcode-block%5Fcodeblockconfig-CodeBlockConfig.html#member-indentSequence) configuration. * The `'outdentCodeBlock'` command is enabled when the indentation of any code lines within the selection can be decreased. Executing it will remove the indentation character (sequence) from these lines, as configured by [codeBlock.indentSequence](../api/module%5Fcode-block%5Fcodeblockconfig-CodeBlockConfig.html#member-indentSequence). ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/collaboration/annotations/annotations-custom-configuration.html" ## Configuration of annotations Tweaking the configuration of CKEditor 5 collaboration features is another easy way to change the behavior of the collaboration features views. ### Collaboration features configuration The full documentation of available configuration options can be found in the [comments feature editor configuration](../../../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-comments), the [track changes feature editor configuration](../../../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-trackChanges) and the [sidebar feature editor configuration](../../../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-sidebar) guides. Note that comments configuration also applies to comments in a suggestion thread. ### Comment editor configuration The editor used for adding and editing comments is also a CKEditor 5 instance. By default, it uses the following plugins: * [Essentials](../../../api/module%5Fessentials%5Fessentials-Essentials.html), * [Paragraph](../../../api/module%5Fparagraph%5Fparagraph-Paragraph.html), These plugins allow for creating the comment content with some basic styles. However, it is possible to extend the comment editor configuration and add some extra plugins or even overwrite the entire configuration and replace the list of plugins. You can modify the comment editor configuration by using the [comments.editorConfig](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html#member-editorConfig) property in the main editor configuration. See the sample below to learn how to add the [Mention](../../../api/module%5Fmention%5Fmention-Mention.html) plugin to the comment editor: Note that additional plugins need to be a part of the same package, just like the main editor plugins. By default, the `ckeditor5` package carries all open-source plugins, while the `ckeditor5-premium-plugins` one keeps all premium plugins at hand. ### Sidebar configuration The sidebar configuration allows for setting the container element for the sidebar and tuning the positioning mechanism. Check the [sidebar configuration](../../../api/module%5Fcomments%5Fconfig-AnnotationsSidebarConfig.html) API documentation for all available options. #### Example of use ``` // ... const editorConfig = { // ... comments: { // Show the first comment and the two most recent comments when collapsed. maxCommentsWhenCollapsed: 3, // Make comments shorter when collapsed. maxCommentCharsWhenCollapsed: 100, // Allow for up to 3 comments before collapsing. maxThreadTotalWeight: 600, // You may set the comments editor configuration. // In this case, use the default configuration. editorConfig: {} }, sidebar: { container: document.querySelector( '#sidebar' ), preventScrollOutOfView: true }, // ... }; ClassicEditor.create( editorConfig ); ``` Using the [comments.CommentThreadView](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html#member-CommentThreadView) and [comments.CommentView](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html#member-CommentView) configuration options is described in the [Annotations custom view](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-view.html) guide. ### Custom date format The comments feature allows you to set a custom date format for comments and suggestions. To enable that, pass a function to the [locale.dateTimeFormat](../../../api/module%5Fcollaboration-core%5Fconfig-LocaleConfig.html#member-dateTimeFormat) property in the main editor configuration. This function is invoked with one argument: a comment or suggestion creation date. ``` // You can use any other library, like moment.js. import { DateTime } from 'luxon'; // ... const editorConfig = { // ... locale: { dateTimeFormat: date => DateTime.fromJSDate( date ).toFormat( 'dd/LL/yyyy' ) } // ... }; ClassicEditor.create( editorConfig ); ``` source file: "ckeditor5/latest/features/collaboration/annotations/annotations-custom-template.html" ## Custom template for annotations A custom template is a middle ground between the default UI and a completely custom UI of your own. ### How it works This solution lets you alter the HTML structure of comment and suggestion annotations. Thanks to that you can, for example: * Add new CSS classes or HTML elements to enable more complex styling. * Re-arrange annotation views. * Add new UI elements, linked with your custom features. * Remove UI elements. ### Views for comments and suggestions The view classes used by default by CKEditor 5 collaboration features are: * [CommentThreadView](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fcommentthreadview-CommentThreadView.html) – Generates the UI for comment thread annotations. * [CommentView](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fcommentview-CommentView.html) – Presents a single comment. * [SuggestionThreadView](../../../api/module%5Ftrack-changes%5Fui%5Fview%5Fsuggestionthreadview-SuggestionThreadView.html) – Generates the UI for suggestion annotations. These closed-source classes are exported by the `ckeditor5-premium-features` package on npm ([learn more](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--setting-up-a-sample-project)). ### Changing the view template To use a custom view template, you need to: 1. Create a new view class by extending the default view class. 2. Provide (or extend) the template by overwriting the [getTemplate()](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fcommentview-CommentView.html#function-getTemplate) method. 3. Set your custom view class through the editor configuration. We recommend setting it in the default configuration. Creating a custom view: Using the custom view in the editor configuration: ``` // ... const editorConfig = { // ... comments: { CommentView: MyCommentView }, // ... }; ClassicEditor.create( editorConfig ); ``` Custom [comment](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html#member-CommentThreadView) and [suggestion](../../../api/module%5Ftrack-changes%5Ftrackchangesconfig-TrackChangesConfig.html#member-SuggestionThreadView) thread views can be set in a similar way: ``` // ... const editorConfig = { // ... comments: { CommentThreadView: MyCommentThreadView, }, trackChanges: { SuggestionThreadView: MySuggestionThreadView }, // ... }; ClassicEditor.create( editorConfig ); ``` ### Example: Adding a new button Below is an example of how you can provide a simple custom feature that requires creating a new UI element and additional styling. The proposed feature should allow for marking some comments as important. An important comment should have a yellow border on the right. To bring this feature, you need: * A CSS class to change the border of an important comment. * A button to toggle the comment model state. * An integration between the model state and the view template. #### Styling for an important comment This step is easy. Add the following styles to the `style.css` file in your project: ``` /* style.css */ /* Yellow border for an important comment. */ .ck-comment--important { border-right: 3px solid hsl( 55, 98%, 48% ); } ``` #### Creating a button and adding it to the template In this step, you will add some new elements to the template. Keep in mind that you do not need to overwrite the whole template. You can extend it instead. #### Enabling the custom view The custom view will be enabled in editor configuration, as [shown earlier](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-template.html--changing-the-view-template). ``` // main.js // ... const editorConfig = { // ... comments: { CommentView: ImportantCommentView, editorConfig: { plugins: [ Essentials, Paragraph, Bold, Italic ] } } }; ClassicEditor.create( editorConfig ); ``` #### Live demo Editor output: ``` ``` Threads: ``` ``` source file: "ckeditor5/latest/features/collaboration/annotations/annotations-custom-theme.html" ## Custom annotation theme This is the simplest way to change the look of annotations. Using the power of [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fvariables), it is really easy to override the default design of comments. You can undo this by adding an extra `.css` file. The image above shows you which variables are responsible for every component of the default annotation view. ### Example of comments customization with CSS variables With the [inheritance of CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fcustom%5Fproperties#Inheritance%5Fof%5Fcustom%5Fproperties), you can change the default `:root` values of the variables in the `.ck-sidebar` scope. You can override these properties with a `.css` file or place your customizations directly into the `` section of your page, but in this case, you will need to use a more specific CSS selector than `:root` (for example, ``). ``` /* Change the default yellow color of the comment marker in the content to green. */ :root { --ck-color-comment-marker: hsl(127, 98%, 83%); --ck-color-comment-marker-active: hsl(127, 98%, 68%); } .ck-sidebar { --ck-color-comment-background: #ecf5f0; --ck-color-comment-separator: #64ca6d; --ck-color-comment-remove-background: #eccbcb; --ck-comment-content-font-family: Arial; --ck-comment-content-font-size: 13px; --ck-comment-content-font-color: #333; --ck-color-comment-count: #807e81; --ck-color-annotation-icon: #0f5c2f; --ck-color-annotation-info: #1eb35c; --ck-annotation-button-size: 0.85em; --ck-user-avatar-background: #239855; } /* You can even change the appearance of a single element. */ .ck-sidebar .ck-comment__wrapper:first-of-type { --ck-color-annotation-info: #8e9822; --ck-user-avatar-background: #8e9822; } ``` The examples above will generate the following designs for comments: source file: "ckeditor5/latest/features/collaboration/annotations/annotations-custom-view.html" ## Custom view for annotations A custom view is the most powerful way to customize annotations. In this case, you need to provide the whole view: a template, UI elements, and any necessary behavior logic, but you can still use some of the default building blocks. ### Using base views Providing a custom view is based on the same solution as [providing a custom template](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-template.html). You will need to create your own class for the view. In this case, you will be interested in extending base view classes: * [BaseCommentThreadView](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fbasecommentthreadview-BaseCommentThreadView.html) – Base view for comment thread views. * [BaseCommentView](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fbasecommentview-BaseCommentView.html) – Base view for comment views. * [BaseSuggestionThreadView](../../../api/module%5Ftrack-changes%5Fui%5Fview%5Fbasesuggestionthreadview-BaseSuggestionThreadView.html) – Base view for suggestion thread views. Base view classes provide some core functionality that is necessary for the view to operate, no matter what the view looks like or what its template is. The default view classes also extend the base view classes. ### Default view template The default template used to create comment thread views is shown here: [CommentThreadView#getTemplate()](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fcommentthreadview-CommentThreadView.html#function-getTemplate). The default template used to create suggestion thread views is shown here: [SuggestionThreadView#getTemplate()](../../../api/module%5Ftrack-changes%5Fui%5Fview%5Fsuggestionthreadview-SuggestionThreadView.html#function-getTemplate). ### Creating and enabling a custom view As a reminder, the code snippet below shows how to enable a custom view for CKEditor 5 collaboration features: The only obligatory action that needs to be done in your custom view constructor is setting up a template: ``` class CustomCommentThreadView extends BaseCommentThreadView { constructor( ...args ) { super( ...args ); this.setTemplate( { // Template definition here. // ... } ); } } ``` It is your responsibility to construct the template and all the UI elements that are needed for the view. #### Reading data and binding with template Your view is passed a model object that is available under the `_model` property. It should be used to set (or bind to) the initial data of your view’s properties. Some of your view’s properties may be used to control the view template. For example, you can bind a view property so that when it is set to `true`, the template main element will receive an additional CSS class. To bind view properties with a template, the properties need to be [observable](../../../api/module%5Futils%5Fobservablemixin-Observable.html#function-set:KEY%5FVALUE). Of course, you can bind already existing observable properties with your template. You can [bind two observable properties](../../../api/module%5Futils%5Fobservablemixin-Observable.html#function-bind:SINGLE%5FBIND) in a way that the value of one property will depend on the other. You can also directly [listen to the changes of an observable property](../../../api/module%5Futils%5Fobservablemixin-Observable.html#event-change:%7Bproperty%7D). ``` const bind = this.bindTemplate; // Set an observable property. this.set( 'isImportant', false ); // More code. // ... this.setTemplate( { tag: 'div', attributes: { class: [ // Bind the new observable property with the template. bind.if( 'isImportant', 'ck-comment--important' ), // Bind an existing observable property with the template. bind.if( 'isDirty', 'ck-comment--unsaved' ) ] } } ); ``` #### Performing actions The view needs to communicate with other parts of the system. When a user performs an action, something needs to be executed, for example: a comment should be removed. This communication is achieved by firing events (with appropriate data). See the example below: ``` this.removeButton = new ButtonView(); this.removeButton.on( 'execute', () => this.fire( 'removeCommentThread' ) ); ``` The list of all events that a given view class can fire is available in the [API documentation of the BaseCommentThreadView class](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fbasecommentthreadview-BaseCommentThreadView.html). ### Example: Comment thread actions dropdown In this example, you will create a custom comment thread view with action buttons (edit, remove) moved to a dropdown UI element. The dropdown will be added inside a new element, and placed above the thread UI. #### Creating a custom thread view with a new template First, create a foundation for your custom solution: Then, you need to create a dropdown UI element and fill it with items: Note that the dropdown should not be visible if the current local user is not the author of the thread. Since the first comment in the comment thread represents the whole thread, you can base it on the properties of the first comment. If there are no comments in the thread, it means that this is a new thread so the local user is the author. ``` class CustomCommentThreadView extends BaseCommentThreadView { constructor( ...args ) { super( ...args ); const bind = this.bindTemplate; // The template definition is partially based on the default comment thread view. const templateDefinition = { tag: 'div', attributes: { class: [ 'ck', 'ck-thread', 'ck-reset_all-excluded', 'ck-rounded-corners', bind.if( 'isActive', 'ck-thread--active' ) ], // Needed for the native DOM Tab key navigation. tabindex: 0, role: 'listitem', 'aria-label': bind.to( 'ariaLabel' ), 'aria-describedby': this.ariaDescriptionView.id }, children: [ { tag: 'div', attributes: { class: 'ck-thread__container' }, children: [ this.commentsListView, this.commentThreadInputView ] } ] }; const isNewThread = this.length == 0; const isAuthor = isNewThread || this._localUser == this._model.comments.get( 0 ).author; // Add the actions dropdown only if the local user is the author of the comment thread. if ( isAuthor ) { templateDefinition.children.unshift( { tag: 'div', attributes: { class: 'ck-thread-top-bar' }, children: [ this._createActionsDropdown() ] } ); } this.setTemplate( templateDefinition ); } // ... } ``` As far as disabling the UI is concerned, the actions in the dropdown should be disabled if the comment thread is in the read-only mode. Also, the edit button should be hidden if there are no comments in the thread. Additionally, the resolve/reopen button should be displayed based on the comment thread resolve state. ``` // main.js class CustomCommentThreadView extends BaseCommentThreadView { // ... _createActionsDropdown() { // ... const editButtonModel = new UIModel( { withText: true, label: 'Edit', action: 'edit' } ); // The button should be enabled when the read-only mode is off. // So, `isEnabled` should be a negative of `isReadOnly`. editButtonModel.bind( 'isEnabled' ) .to( this._model, 'isReadOnly', isReadOnly => !isReadOnly ); // Hide the button if the thread has no comments yet. editButtonModel.bind( 'isVisible' ) .to( this, 'length', length => length > 0 ); items.add( { type: 'button', model: editButtonModel } ); const resolveButtonModel = new UIModel( { withText: true, label: 'Resolve', action: 'resolve' } ); // Hide the button if the thread is resolved or cannot be resolved. resolveButtonModel.bind( 'isVisible' ) .to( this._model, 'isResolved', this._model, 'isResolvable', ( isResolved, isResolvable ) => !isResolved && isResolvable ); items.add( { type: 'button', model: resolveButtonModel } ); const reopenButtonModel = new UIModel( { withText: true, label: 'Reopen', action: 'reopen' } ); // Hide the button if the thread is not resolved or cannot be resolved. reopenButtonModel.bind( 'isVisible' ) .to( this._model, 'isResolved', this._model, 'isResolvable', ( isResolved, isResolvable ) => isResolved && isResolvable ); items.add( { type: 'button', model: reopenButtonModel } ); const removeButtonModel = new UIModel( { withText: true, label: 'Delete', action: 'delete' } ); removeButtonModel.bind( 'isEnabled' ) .to( this._model, 'isReadOnly', isReadOnly => !isReadOnly ); items.add( { type: 'button', model: removeButtonModel } ); } } ``` Finally, some styling will be required for the new UI elements: ``` /* style.css */ /* ... */ .ck-thread-top-bar { padding: 2px 4px 3px 4px; background: #404040; text-align: right; } .ck-thread-top-bar .ck.ck-dropdown { font-size: 14px; width: 100px; } .ck-thread-top-bar .ck.ck-dropdown .ck-button.ck-dropdown__button { color: #000000; background: #EEEEEE; } ``` #### Linking buttons with actions The edit button should turn the first comment into edit mode: ``` dropdownView.on( 'execute', evt => { const action = evt.source.action; if ( action == 'edit' ) { this.commentsListView.commentViews.get( 0 ).switchToEditMode(); } // More actions. // ... } ); ``` The delete button should remove the comment thread. As described earlier, your view should fire events to communicate with other parts of the system: ``` dropdownView.on( 'execute', evt => { const action = evt.source.action; if ( action == 'edit' ) { this.commentsListView.commentViews.get( 0 ).switchToEditMode(); } if ( action == 'delete' ) { this.fire( 'removeCommentThread' ); } if ( action == 'resolve' ) { this.fire( 'resolveCommentThread' ); } if ( action == 'reopen' ) { this.fire( 'reopenCommentThread' ); } if ( action == 'resolve' ) { this.fire( 'resolveCommentThread' ); } if ( action == 'reopen' ) { this.fire( 'reopenCommentThread' ); } } ); ``` #### Altering the first comment view Your new custom comment thread view is ready. For comment views, you will use the default comment views. However, there is one thing you need to take care of. Since you moved comment thread controls to a separate dropdown, you should hide these buttons from the first comment view. This modification will be added in a custom comment thread view. It should not be done in a custom comment view because that would have an impact on comments in suggestion threads. The first comment view can be obtained from the [commentsListView](../../../api/module%5Fcomments%5Fcomments%5Fui%5Fview%5Fbasecommentthreadview-BaseCommentThreadView.html#member-commentsListView) property. If there are no comments yet, you can listen to the property and apply the custom behavior when the first comment view is added. ``` // main.js class CustomCommentThreadView extends BaseCommentThreadView { constructor( ...args ) { // More code. // ... if ( this.length > 0 ) { // If there is a comment when the thread is created, apply custom behavior to it. this._modifyFirstCommentView(); } else { // If there are no comments (an empty thread was created by the user), // listen to `this.commentsListView` and wait for the first comment to be added. this.listenTo( this.commentsListView.commentViews, 'add', evt => { // And apply the custom behavior when it is added. this._modifyFirstCommentView(); evt.off(); } ); } } // More code. // ... _modifyFirstCommentView() { // Get the first comment. const commentView = this.commentsListView.commentViews.get( 0 ); // By default, the comment button is bound to the model state // and the buttons are visible only if the current local user is the author. // You need to remove this binding and make buttons for the first // comment always invisible. commentView.removeButton.unbind( 'isVisible' ); commentView.removeButton.isVisible = false; commentView.editButton.unbind( 'isVisible' ); commentView.editButton.isVisible = false; } } ``` #### Final solution Below you can find the final code for the created components: ``` /* style.css */ /* ... */ .ck-thread-top-bar { padding: 2px 4px 3px 4px; background: #404040; text-align: right; } .ck-thread-top-bar .ck.ck-dropdown { font-size: 14px; width: 100px; } .ck-thread-top-bar .ck.ck-dropdown .ck-button.ck-dropdown__button { color: #000000; background: #EEEEEE; } ``` #### Live demo source file: "ckeditor5/latest/features/collaboration/annotations/annotations-display-mode.html" ## Annotations display mode There are three built-in UIs to display comment threads and suggestion annotations: the wide sidebar, the narrow sidebar, and inline balloons. You can also display them together in more advanced scenarios where various annotation sources (comments, suggestions) are connected to different UIs, or even create your own UI for annotations. ### Inline balloons Inline balloon display mode is designed for narrow screens like mobile devices and UIs where the WYSIWYG editor is used to edit a small part of the content. Inline display mode is the default solution. It is used when the sidebar configuration is not specified. Even if the sidebar configuration is set, you can still dynamically switch to the inline display mode by calling the [switchTo()](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUIs.html#function-switchTo) method with the `'inline'` argument: ``` // ... ClassicEditor .create( editorConfig ) .then( editor => { editor.plugins.get( 'AnnotationsUIs' ).switchTo( 'inline' ); // The sidebar container is not removed automatically, // so it is up to your integration to hide it (or manage in another way). document.querySelector( '.editor-container__sidebar' ).style.display = 'none'; } ); ``` ### Wide sidebar The wide sidebar can fit the largest amount of information. In this mode the user can see the beginning and the end of each discussion, as well as the entire discussion for the currently selected marker. It is the recommended solution whenever you have enough space for it. To use the wide sidebar for displaying comments and suggestion annotations, first, prepare a proper HTML structure: ``` CKEditor 5 Sample
``` Then update CSS styles to display the sidebar on the right side of the editor: ``` .editor-container { --ckeditor5-preview-sidebar-width: 270px; } .editor-container__editor-wrapper { display: flex; width: fit-content; } .editor-container--classic-editor .editor-container__editor { min-width: 795px; max-width: 795px; } .editor-container__sidebar { min-width: var(--ckeditor5-preview-sidebar-width); max-width: var(--ckeditor5-preview-sidebar-width); margin-top: 28px; margin-left: 10px; margin-right: 10px; } ``` Then, initialize the rich text editor. In the configuration, set the editor to use the `
` element as the comments container. ``` // ... const editorConfig = { // ... sidebar: { container: document.querySelector('#editor-annotations') }, // ... } ClassicEditor.create( editorConfig ); ``` After setting the configuration as shown in the example above, the wide sidebar display mode will be used. If the display mode was changed, you can change it back by calling [switchTo()](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUIs.html#function-switchTo): ``` ClassicEditor .create( editorConfig ) .then( editor => { editor.plugins.get( 'AnnotationsUIs' ).switchTo( 'wideSidebar' ); } ); ``` You can also set the sidebar container dynamically using the [setContainer()](../../../api/module%5Fcomments%5Fannotations%5Fsidebar-Sidebar.html#function-setContainer) method: ``` ClassicEditor .create( editorConfig ) .then( editor => { editor.plugins.get( 'Sidebar' ).setContainer( element ); } ); ``` If the sidebar container has already been set, all the items inside it will be moved to the new container. ### Narrow sidebar The narrow sidebar is a compromise between the wide sidebar and the inline balloons. It does not take as much space as the wide sidebar but contains more information than inline annotations. The user will immediately see when multiple comment threads are added to the same spot as well as how many comments are added. The HTML structure for the wide and narrow sidebars is similar. The only difference is that you need to set a different `min-width` CSS property for the `#editor-annotations` element: ``` /* ... */ .editor-container__sidebar { min-width: 65px; } /* ... */ ``` Then, initialize the editor and switch the UI to the `narrowSidebar` mode using the [switchTo()](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUIs.html#function-switchTo) method. Note that you need to switch the UI type manually since the wide sidebar will be displayed by default. ``` // ... const editorConfig = { // ... sidebar: { container: document.querySelector('#editor-annotations') }, // ... } ClassicEditor .create( editorConfig ) .then( editor => { editor.plugins.get( 'AnnotationsUIs' ).switchTo( 'narrowSidebar' ); } ); ``` ### Multiple UIs Annotations were designed to support displaying various annotations UIs at the same time. This allows you to display different annotation sources in various places, for example, displaying comments in the wide sidebar while showing inline balloons for suggestions. To activate multiple UIs at the same time, the filtering function should be passed to the [activate()](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUIs.html#function-activate) method. The function specifies which annotations are controlled by a given UI. Note that one annotation cannot be managed by multiple [AnnotationsUI](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUI.html) at the same time. If an annotation is not accepted by any of annotations UIs, then that annotation will not be shown. To use a combination of annotations UIs for displaying comments and suggestion annotations, first prepare a proper HTML structure (for demonstration purposes, the wide sidebar is used): ```
``` Then, initialize the rich text editor using a preset that includes both the comments and track changes features. You can get the necessary code from the [track changes integration](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html) guide. Then tweak the code to use two annotations UIs as shown below: ``` // ... ClassicEditor .create( editorConfig ) .then( editor => { const annotationsUIs = editor.plugins.get( 'AnnotationsUIs' ); // Deactivate all UIs first as the `activate()` method might not deactivate all UIs. annotationsUIs.deactivateAll(); annotationsUIs.activate( 'wideSidebar', annotation => annotation.type === 'comment' ); annotationsUIs.activate( 'inline', annotation => annotation.type !== 'comment' ); } ); ```
### All display modes in action The code snippet below allows for switching between all available display modes. In the `index.html` file obtained from the Builder, add the following markup: ```
``` Then, in the `style.css` file, add the following styles ``` .editor-container__sidebar { transition: min-width .4s ease-out-in; } .editor-container__sidebar.narrow { min-width: 65px; } .editor-container__sidebar.hidden { display: none; } ``` Finally, add the following code in the `main.js` file: ``` ClassicEditor .create( editorConfig ) .then( editor => { const annotationsUIs = editor.plugins.get( 'AnnotationsUIs' ); const annotationsContainer = document.querySelector( '.editor-container__sidebar' ); const inlineButton = document.querySelector( '#inline' ); const narrowButton = document.querySelector( '#narrow' ); const wideButton = document.querySelector( '#wide' ); const wideAndInlineButton = document.querySelector( '#wide-inline' ); function markActiveButton( button ) { [ inlineButton, narrowButton, wideButton, wideAndInlineButton ] .forEach( el => el.classList.toggle( 'active', el === button ) ); } function switchToInline() { markActiveButton( inlineButton ); annotationsContainer.classList.remove( 'narrow' ); annotationsContainer.classList.add( 'hidden' ); annotationsUIs.switchTo( 'inline' ); } function switchToNarrowSidebar() { markActiveButton( narrowButton ); annotationsContainer.classList.remove( 'hidden' ); annotationsContainer.classList.add( 'narrow' ); annotationsUIs.switchTo( 'narrowSidebar' ); } function switchToWideSidebar() { markActiveButton( wideButton ); annotationsContainer.classList.remove( 'narrow', 'hidden' ); annotationsUIs.switchTo( 'wideSidebar' ); } function switchToWideSidebarAndInline() { markActiveButton( wideAndInlineButton ); annotationsContainer.classList.remove( 'narrow', 'hidden' ); annotationsUIs.deactivateAll(); annotationsUIs.activate( 'wideSidebar', annotation => annotation.type === 'comment' ); annotationsUIs.activate( 'inline', annotation => annotation.type !== 'comment' ); } editor.ui.view.listenTo( inlineButton, 'click', () => switchToInline() ); editor.ui.view.listenTo( narrowButton, 'click', () => switchToNarrowSidebar() ); editor.ui.view.listenTo( wideButton, 'click', () => switchToWideSidebar() ); editor.ui.view.listenTo( wideAndInlineButton, 'click', () => switchToWideSidebarAndInline() ); // Set wide sidebar as default. switchToWideSidebar(); } ) .catch( error => console.error( error ) ); ```
#### Demo The following sample showcases the snippet above: ### Custom UI In addition to the built-in annotations UIs, it is also possible to create a custom UI that will display annotations in a way that is better suited to your application. Note that annotations UI should implement the [AnnotationsUI](../../../api/module%5Fcomments%5Fannotations%5Fannotationsuis-AnnotationsUI.html) interface. The frame of an annotations UI is presented below. For this to work, it must be included in the list of editor plugins and activated. These are the changes that you will have to make to the `main.js` file of your project: source file: "ckeditor5/latest/features/collaboration/annotations/annotations.html" ## Annotations in CKEditor 5 collaboration features Annotations are a part of the collaboration features system. They are UI elements (“balloons”) that correspond to comments and suggestions. ### Additional feature information Features like comments and track changes create views (“balloons”) that represent their data. Such a view is called an annotation. They are added to and stored in the annotations plugin. Then, UI mechanisms (like sidebars or inline annotations) use the annotation views to populate themselves and create various types of user experiences. Using the annotations system and the provided API, you can: * [Choose between the provided display modes (sidebars or inline annotations)](#ckeditor5/latest/features/collaboration/annotations/annotations-display-mode.html). * [Customize annotations created by comments and track changes plugins](#ckeditor5/latest/features/collaboration/annotations/annotations.html--annotations-customization). * Provide custom annotations for your plugins. * Provide custom UI for annotations (for example, a custom sidebar with your display logic). ### Annotations customization There are multiple levels on which you can modify the look of annotations: * [Theme customization](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-theme.html). * [Configuration, including comment input field configuration](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-configuration.html). * [Providing a custom template for the default views](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-template.html). * [Providing a custom view for annotations](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-view.html). Refer to the linked guides to learn more about how to customize annotations for collaboration features of CKEditor 5. ### API overview The main entry point for all external actions should be the [Annotations](../../../api/module%5Fcomments%5Fannotations%5Fannotations-Annotations.html) plugin. It stores [annotations](../../../api/module%5Fcomments%5Fannotations%5Fannotation-Annotation.html) for all editors and allows manipulating them. In this example, the [Annotation plugin API](../../../api/module%5Fcomments%5Fannotations%5Fannotations-Annotations.html) will be used to display a custom annotation. To do that, you should create a target element to which the annotation will be attached. In the `index.html` file created in the [reference](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--before-you-start) guide, add the following static `
` element next to the editor data container: ```
Custom annotation target
``` Now, in the `main.js` file of the project, please add the following code that creates the annotation: When you run the project, you should see the “Custom annotation target” element displayed below the editor. You should also see the annotation view with the “Annotation text” displayed in the sidebar.
source file: "ckeditor5/latest/features/collaboration/collaboration.html" ## Collaboration overview The CKEditor 5 architecture was designed to bring collaborative editing features where many authors can work on the same rich text documents. ### Demo Use the set of collaboration features in the demo below: turn on tracking changes , add comments , check the comments archive , and follow the revision history of the document. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Available collaboration features The collaboration capabilities are provided by three easy-to-integrate plugins delivering different features: comments, track changes, and revision history. You will find more information about each feature in the dedicated guides. You may also look at some interesting details and examples in the [Collaborative writing in CKEditor 5](https://ckeditor.com/blog/Feature-of-the-month-Collaborative-writing-in-CKEditor-5/) blog post after reading these guides. You can use these features standalone or together, depending on the users’ needs. The collaboration can also be [either in real time or asynchronous](#ckeditor5/latest/features/collaboration/collaboration.html--real-time-vs-asynchronous-collaboration). #### Comments Thanks to the comments feature, the users can add sidenotes to marked fragments of the document, including text and block elements such as images. It also allows the users to discuss in threads and remove comments when they finish the discussion. You can define where you want to store the comments data. To load and save it, you will also need to create a proper [integration with your database](#ckeditor5/latest/features/collaboration/comments/comments-integration.html). If you want to automatically synchronize the comments discussion between users, you can also use comments as a part of the real-time collaboration. You can display comment threads in a sidebar or inline: Moreover, you can resolve comment threads, which moves them to the archive. Note that the comments archive is enabled by default and cannot be turned off. Refer to the [Comments](#ckeditor5/latest/features/collaboration/comments/comments.html) guide for more information. #### Track changes The track changes feature brings automatic suggestion marking for the document as you change it. When editing the document, the user can switch to the track changes mode. All their changes will then create suggestions that they can accept or discard. You can define where you want to store the suggestions data. To load and save it, you will also need to create a proper [integration with your database](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html). If you want to automatically synchronize the suggestions between users, you can also use track changes as a part of the real-time collaboration. You can display suggestion annotations in a sidebar or inline: Refer to the [Track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) guide for more information. #### Revision history The revision history feature is a document versioning tool. It allows CKEditor 5 users to create and view the chronological revision history of their content. These versions are listed in the side panel. The preview mode allows for easy viewing of content development between revisions. You can rename, compare, and restore older revisions on the go. Refer to the [revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history.html) guide for more information. ### Real-time vs asynchronous collaboration There are two available collaboration modes in CKEditor 5: real-time collaboration (often referred to as RTC) and asynchronous collaboration. Both collaborative workflows allow your users to work together within a single application, without the need for third-party tools. They can either collaborate on documents asynchronously or use a real-time editor to write, review, and comment on content in live mode. You can use all available CKEditor 5 collaboration plugins in both modes. | **Asynchronous vs real-time collaboration comparison** | | | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | Collaboration type | **Asynchronous** | **Real-time** | | Workflow | Sequential | Simultaneous | | Features included | Revision history, track changes, and comments. | Revision history, track changes, and comments working in real time. | | Backend | Custom backend provided by the customer. | Backend provided by CKEditor in on-premises and SaaS modes. | | Conflict solving | Not implemented. | Automatically solves all conflicts if users make changes at the same time. | | Integration tasks for system developers | Write backend endpoints to save and load data.Write frontend adapters to pass the data to backend endpoints. | Just configure tokens for the SaaS version.For on-premises, also set up the infrastructure. | #### Asynchronous collaboration Asynchronous collaboration is perfect for linear workflow, where users create, review, and edit content sequentially and there is no need for them to work simultaneously. It suits professional environments working on business deals, legal documents, academic research papers, contract management, and more use cases. In this mode, a single author can work on the document, using the revision history, track changes and comments features to interact with previous and following editors. All work is done sequentially. The asynchronous approach can be more cost-effective and it requires a less dedicated infrastructure. It also gives you full control over your data. Because you are fully responsible for loading, saving, and storing the data, it is the on-premises version by default. On the other hand, this approach requires you to maintain both frontend and backend integration code, also in a situation when the editor features are updated. #### Real-time collaboration In real-time collaboration, on the other hand, many users can work simultaneously on the same document, even on the same part of it, with no content locking. Comments and track changes are synchronized automatically between users, on the go. It automatically solves all conflicts that may occur if users make changes at the same time. The editor also lists all users currently involved in the editing process. Thanks to this, collaborating users will not only be able to edit a rich text document at the same time but also discuss the process live in comments. They can also save revisions. This is perfect for fast-paced content-creation situations and it can still prove useful in a single-user mode, just like the asynchronous solution. Real-time collaboration comes with a ready-to-use frontend integration and a backend solution. You can use it as SaaS with CKEditor Cloud Services or install on your machines in the on-premises version. CKEditor provides both, so there is no need for a complicated integration. Import the plugins, fill in the editor configuration, and provide the token configuration. The on-premises solution requires some minimal extra setup. You can still maintain control over your data. REST APIs will allow you to copy whatever data you stored on our servers and more! Refer to the [Real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) guide for more information. source file: "ckeditor5/latest/features/collaboration/comments/comments-archive.html" ## Comments archive custom UI By default, the comments archive dropdown panel is displayed in the toolbar by adding the `'commentsArchive'` button in the toolbar configuration. Refer to the [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) guide for more information. In this guide, you will learn how to prepare a custom comments archive UI in your application and display it in the container of your choice. ### Before you start For the purpose of this guide, the CKEditor Cloud Services and the [real-time collaborative comments](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) feature will be used. However, the comments feature API can also be used in a similar way together with the [standalone comments](#ckeditor5/latest/features/collaboration/comments/comments-integration.html) feature. Make sure that your editor is properly integrated with the comments feature before moving on. ### Preparing the HTML structure In this guide, we will prepare an editor integration with a custom side panel. There will be two tabs that will let the user switch between what is displayed in the side panel. By default, the side panel will display the editor’s regular wide sidebar. The other tab will switch the side panel content to display the resolved comments. First, adjust the HTML structure by extending the `div.editor-container__sidebar` container: ```
...
...
``` Then add styles for the side panel: ``` ```
### Implementing the custom comments archive UI plugin Now, create a plugin that will use the provided HTML structure and fill the comments archive container with resolved comment threads. The behavior for the tabs will be implemented as simple DOM event listeners. You can observe changes on the [CommentsArchiveUI#annotationViews](../../../api/module%5Fcomments%5Fcomments%5Fcommentsarchiveui-CommentsArchiveUI.html#member-annotationViews) collection to fill the comments archive tab content. Additionally, for a better user experience, as long as the comments archive is shown in the side panel, the annotations for regular comment threads will be displayed in the inline display mode. This will give the users access to the regular comment threads data also when the archive is open. ``` class CustomCommentsArchiveUI extends Plugin { static get requires() { // We will use a property from the `CommentsArchiveUI` plugin, so add it to requires. return [ 'CommentsArchiveUI' ]; } init() { this.tabs = document.querySelectorAll( '.tabs__item' ); this.sidebars = document.querySelectorAll( '.sidebar' ); // Switch the side panel to the appropriate tab after clicking it. this.tabs.forEach( item => { item.addEventListener( 'click', () => this.handleTabClick( item ) ); } ); this.initCommentsArchive(); } // Switches between the active tabs. // Shows appropriate tab container and set the CSS classes to reflect the changes. handleTabClick( tabElement ) { if ( tabElement.classList.contains( 'active' ) ) { return; } const annotationsUIs = this.editor.plugins.get( 'AnnotationsUIs' ); const targetId = tabElement.dataset.target; const sidebarContainer = document.getElementById( targetId ); this.tabs.forEach( item => { item.classList.remove( 'active' ); } ); this.sidebars.forEach( item => { item.classList.remove( 'active' ); } ); tabElement.classList.add( 'active' ); sidebarContainer.classList.add( 'active' ); const isCommentsArchiveOpen = targetId === 'archive'; // If the comments archive is open, switch the display mode for comments to "inline". // // This way the annotations for regular comments threads will be displayed next to them // when a user clicks on the comment thread marker. // // When the comments archive is closed, switch back to displaying comments annotations in the wide sidebar. annotationsUIs.switchTo( isCommentsArchiveOpen ? 'inline' : 'wideSidebar' ); } initCommentsArchive() { // Container for the resolved comment threads annotations. const commentsArchiveList = document.querySelector( '.comments-archive__list' ); // The `CommentsArchiveUI` plugin handles all annotation views that can be used // to render resolved comment threads inside the comments archive container. const commentsArchiveUI = this.editor.plugins.get( 'CommentsArchiveUI' ); // First, handle the initial resolved comment threads. for ( const annotationView of commentsArchiveUI.annotationViews ) { commentsArchiveList.appendChild( annotationView.element ); } // Handler to append new resolved thread inside the comments archive custom view. commentsArchiveUI.annotationViews.on( 'add', ( _, annotationView ) => { if ( !commentsArchiveList.contains( annotationView.element ) ) { commentsArchiveList.appendChild( annotationView.element ); } } ); // Handler to remove the element when thread has been removed or reopened. commentsArchiveUI.annotationViews.on( 'remove', ( _, annotationView ) => { if ( commentsArchiveList.contains( annotationView.element ) ) { commentsArchiveList.removeChild( annotationView.element ); } } ); } } ``` Finally, add the new plugin to the editor. ``` ClassicEditor .create( { // ... plugins: [ // ... CustomCommentsArchiveUI ] } ) .then( /* ... */ ) .catch( /* ... */ ); ``` ### Demo Click the “add comment” button in the toolbar to add a comment thread, then use the “tick” icon to resolve a comment thread. Finally, you can see the resolved comment threads in the “Comments archive” tab. source file: "ckeditor5/latest/features/collaboration/comments/comments-integration.html" ## Integrating comments with your application The comments feature [provides an API](../../../api/comments.html) that lets you add, remove, and update comments in the editor. To save and access all these changes in your database, you first need to integrate this feature. ### Integration methods This guide will discuss two ways to integrate CKEditor 5 with your comments data source: * [A simple “load and save” integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--a-simple-load-and-save-integration) using directly the `CommentsRepository` plugin API. * [An adapter integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--adapter-integration) which updates the comments data immediately in the database when it changes in the editor. The adapter integration is the recommended one because it gives you better control over the data. ### Before you start #### Preparing a custom editor setup To use the comments plugin, you need to prepare a custom editor setup with the asynchronous version of the comments feature included. The easiest way to do that is by using the [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs). Pick a preset and start customizing your editor. **In the “Features” section** of the Builder (2nd step), make sure to: * turn off the “real-time” toggle next to the “Collaboration” group, * enable the “Collaboration → Comments” feature. Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step. #### Setting up a sample project Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository: ``` npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project cd sample-project npm install ``` Then, install the necessary dependencies: ``` npm install ckeditor5 npm install ckeditor5-premium-features ``` This project template uses [Vite](https://vitejs.dev/) under the hood and contains 3 source files that we will use: `index.html`, `style.css`, and `main.js`. It is now the time to use our custom editor setup. **Go to the “Installation” section** of the Builder and copy the generated code snippets to those 3 files. #### Activating the feature To use this premium feature, you need to activate it with a license key. Refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. After you have successfully obtained the license key open the `main.js` file and update the `your-license-key` string with your license key. #### Building the project Finally, build the project by running: ``` npm run dev ``` When you open the sample in the browser you should see the WYSIWYG editor with the comments plugin. However, it still does not load or save any data. You will learn how to add data to the comments plugin later in this guide. Let’s now dive deeper into the structure of this setup. #### Basic setup’s anatomy Let’s now go through the key fragments of this basic setup. ##### HTML structure The HTML and CSS structure of the page creates two columns: * `
` is the container used by the editor. * `
` is the container used by the sidebar that holds the annotations (namely comments). ##### JavaScript The `main.js` file sets up the editor instance: * Loads all necessary editor plugins (including the [Comments](../../../api/module%5Fcomments%5Fcomments-Comments.html) plugin). * Sets the `licenseKey` configuration option. * Sets the `sidebar.container` configuration option to the container mentioned above. * Adds the `comment` and `commentsArchive` buttons to the editor toolbar. * Defines the templates for the `CommentsIntegration` and `UsersIntegrations` plugins that we will use in the next steps of this tutorial. #### Comments API The integration below uses the comments API. Making yourself familiar with the API may help you understand the code snippets. In case of any problems, refer to the [comments API documentation](../../../api/comments.html). #### Next steps We have set up a simple JavaScript project that runs a basic CKEditor 5 instance with the asynchronous version of the Comments feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods. ### A simple “load and save” integration In this solution, users and comments data is loaded during the editor initialization, and comments data is saved after you finish working with the editor (for example when you submit the form containing the WYSIWYG editor). This method is recommended if you can trust your users or if you provide additional validation of the submitted data. This way, we can make sure that the user changes their comments only. #### Loading the data When the comments plugin is already included in the editor, you need to create a plugin which will initialize users and existing comments. First, dump the users and comments data to a variable that will be available for your plugin. If you have set up the sample project as [recommended in the “Before you start” section](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--before-you-start), open the `main.js` file and add this variable right after the imports: ``` // Application data will be available under a global variable `appData`. const appData = { // Users data. users: [ { id: 'user-1', name: 'Mex Haddox' }, { id: 'user-2', name: 'Zee Croce' } ], // The ID of the current user. userId: 'user-1', // Comment threads data. commentThreads: [ { threadId: 'thread-1', comments: [ { commentId: 'comment-1', authorId: 'user-1', content: '

Are we sure we want to use a made-up disorder name?

', createdAt: new Date( '09/20/2018 14:21:53' ), attributes: {} }, { commentId: 'comment-2', authorId: 'user-2', content: '

Why not?

', createdAt: new Date( '09/21/2018 08:17:01' ), attributes: {} } ], context: { type: 'text', value: 'Bilingual Personality Disorder' }, unlinkedAt: null, resolvedAt: null, resolvedBy: null, attributes: {} } ], // Editor initial data. initialData: `

Bilingual Personality Disorder

This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth. As recent studies show, the language you speak has more effects on you than you realize. According to the studies, the language a person speaks affects their cognition, behavior, emotions and hence their personality.

This shouldn’t come as a surprise since we already know that different regions of the brain become more active depending on the activity. The structure, information and especially the culture of languages varies substantially and the language a person speaks is an essential element of daily life.

` }; ``` The Builder’s output sample already provides templates of two plugins: `UsersIntegration` and `CommentsIntegration`. Replace them with ones that read the data from `appData` and use the [Users](../../../api/module%5Fcollaboration-core%5Fusers-Users.html) and [CommentsRepository](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html) API: ``` class UsersIntegration extends Plugin { static get requires() { return [ 'Users' ]; } static get pluginName() { return 'UsersIntegration'; } init() { const usersPlugin = this.editor.plugins.get( 'Users' ); // Load the users data. for ( const user of appData.users ) { usersPlugin.addUser( user ); } // Set the current user. usersPlugin.defineMe( appData.userId ); } } class CommentsIntegration extends Plugin { static get requires() { return [ 'CommentsRepository', 'UsersIntegration' ]; } static get pluginName() { return 'CommentsIntegration'; } init() { const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' ); // Load the comment threads data. for ( const commentThread of appData.commentThreads ) { commentsRepositoryPlugin.addCommentThread( commentThread ); } } } ``` Update the `editorConfig.root.initialData` property to use the `appData.initialData` value: ``` const editorConfig = { // ... root: { initialData: appData.initialData } // ... }; ``` And build the project: ``` npm run dev ``` You should now we see an editor instance with one comment thread. #### Saving the data To save the comments data, you need to get it using the `CommentsRepository` API first. To do this, use the [getCommentThreads()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#function-getCommentThreads) method. Then, use the comment threads data to save it in your database in the way you prefer. See the example below. In `index.html` add: ``` ``` In `main.js` update the `ClassicEditor.create()` call with a chained `then()`: ``` ClassicEditor .create( /* ... */ ) .then( editor => { // After the editor is initialized, add an action to be performed after a button is clicked. const commentsRepository = editor.plugins.get( 'CommentsRepository' ); // Get the data on demand. document.querySelector( '#get-data' ).addEventListener( 'click', () => { const editorData = editor.data.get(); const commentThreadsData = commentsRepository.getCommentThreads( { skipNotAttached: true, skipEmpty: true, toJSON: true } ); // Now, use `editorData` and `commentThreadsData` to save the data in your application. // For example, you can set them as values of hidden input fields. console.log( editorData ); console.log( commentThreadsData ); } ); } ); ``` #### Demo ##### Data ``` ``` ##### Comments ``` ``` ### Adapter integration Adapter integration uses an adapter object – provided by you – to immediately save changes in comments in your data store. It is the recommended way of integrating comments with your application because it lets you handle client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side, like the comment creation date. You will see how to handle the server response in the following steps. #### Implementation First, define the adapter using the [CommentsRepository#adapter](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#member-adapter) property. [Adapter methods](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsAdapter.html) are called after the user makes a change in the comments. The adapter allows you to save the change in your database immediately. Each comment action has a separate adapter method that you should implement. On the UI side, each change in comments is performed immediately, however, all adapter actions are asynchronous and are performed in the background. Because of this, all adapter methods need to return a `Promise`. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor throws a [CKEditorError](../../../api/module%5Futils%5Fckeditorerror-CKEditorError.html) error, which works nicely together with the [watchdog](#ckeditor5/latest/features/watchdog.html) feature. When you handle the server response you can decide if the promise should be resolved or rejected. While any adapter action is being performed, a pending action is automatically added to the editor [PendingActions](../../../api/module%5Fcore%5Fpendingactions-PendingActions.html) plugin, so you do not have to worry that the editor will be destroyed before the adapter action has finished. Now you are ready to implement the adapter. If you have set up the sample project as [recommended in the “Before you start” section](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--before-you-start), open the `main.js` file and add this variable right after the imports: ``` // Application data will be available under a global variable `appData`. const appData = { // Users data. users: [ { id: 'user-1', name: 'Mex Haddox' }, { id: 'user-2', name: 'Zee Croce' } ], // The ID of the current user. userId: 'user-1', // Editor initial data. initialData: `

Bilingual Personality Disorder

This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth. As recent studies show, the language you speak has more effects on you than you realize. According to the studies, the language a person speaks affects their cognition, behavior, emotions and hence their personality.

This shouldn’t come as a surprise since we already know that different regions of the brain become more active depending on the activity. The structure, information and especially the culture of languages varies substantially and the language a person speaks is an essential element of daily life.

` }; ``` The Builder’s output sample already provides templates of two plugins `UsersIntegration` and `CommentsIntegration`. Replace them with ones that read the data from `appData` and use the [Users](../../../api/module%5Fcollaboration-core%5Fusers-Users.html) and [CommentsRepository](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html) API: ``` class UsersIntegration extends Plugin { static get requires() { return ['Users']; } static get pluginName() { return 'UsersIntegration'; } init() { const usersPlugin = this.editor.plugins.get( 'Users' ); // Load the users data. for ( const user of appData.users ) { usersPlugin.addUser( user ); } // Set the current user. usersPlugin.defineMe( appData.userId ); } } class CommentsIntegration extends Plugin { static get requires() { return [ 'CommentsRepository', 'UsersIntegration' ]; } static get pluginName() { return 'CommentsIntegration'; } init() { const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' ); // Set the adapter on the `CommentsRepository#adapter` property. commentsRepositoryPlugin.adapter = { addComment( data ) { console.log( 'Comment added', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. // When the promise resolves with the comment data object, it // will update the editor comment using the provided data. return Promise.resolve( { createdAt: new Date() // Should be set on the server side. } ); }, updateComment( data ) { console.log( 'Comment updated', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve(); }, removeComment( data ) { console.log( 'Comment removed', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve(); }, addCommentThread( data ) { console.log( 'Comment thread added', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve( { threadId: data.threadId, comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side. } ); }, getCommentThread( data ) { console.log( 'Getting comment thread', data ); // Write a request to your database here. The returned `Promise` // should resolve with the comment thread data. return Promise.resolve( { threadId: data.threadId, comments: [ { commentId: 'comment-1', authorId: 'user-2', content: '

Are we sure we want to use a made-up disorder name?

', createdAt: new Date(), attributes: {} } ], // It defines the value on which the comment has been created initially. // If it is empty it will be set based on the comment marker. context: { type: 'text', value: 'Bilingual Personality Disorder' }, unlinkedAt: null, resolvedAt: null, resolvedBy: null, attributes: {}, isFromAdapter: true } ); }, updateCommentThread( data ) { console.log( 'Comment thread updated', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve(); }, resolveCommentThread( data ) { console.log( 'Comment thread resolved', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve( { resolvedAt: new Date(), // Should be set on the server side. resolvedBy: usersPlugin.me.id // Should be set on the server side. } ); }, reopenCommentThread( data ) { console.log( 'Comment thread reopened', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve(); }, removeCommentThread( data ) { console.log( 'Comment thread removed', data ); // Write a request to your database here. The returned `Promise` // should be resolved when the request has finished. return Promise.resolve(); } }; } } ``` Update the `editorConfig.root.initialData` property to use `appData.initialData` value: ``` const editorConfig = { // ... root: { initialData: appData.initialData }, // ... }; ``` And build the project: ``` npm run dev ``` You should now we see an editor instance with one comment thread. Observe the browser console while you interact with the comments feature in the editor (add/remove threads and comments). #### Demo ``` ``` Since the comments adapter saves the comment changes immediately after they are performed, it is also recommended to use the [Autosave](../../../api/module%5Fautosave%5Fautosave-Autosave.html) plugin to save the editor content after each change. #### Why is there no event when I remove comment thread markers from the content? Note that no remove event is fired when you remove the marker corresponding to the comment thread. Instead, the comment thread is resolved which triggers [CommentsRepository#resolveCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#event-resolveCommentThread). This operation can be restored using undo (Cmd+Z or Ctrl+Z), which will fire [CommentsRepository#reopenCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#event-reopenCommentThread). However, you can still remove the comment thread by using available buttons in an annotation. Remember that the removal operation cannot be undone. ### Comments samples Please visit the [ckeditor5-collaboration-samples](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master) GitHub repository to find several sample integrations of the comments feature. source file: "ckeditor5/latest/features/collaboration/comments/comments-only-mode.html" ## Comments-only mode In the comments-only mode, you can create, edit, and remove comments but you are unable to change the document content. ### Enabling comments-only mode The recommended way to enable the comments-only mode is by setting proper [user permissions](#ckeditor5/latest/features/collaboration/users.html--user-permissions). If you need to enable or disable this mode based on some other logic (different from the user permissions), you can set it using the [CommentsOnly API](../../../api/module%5Fcomments%5Fcommentsonly-CommentsOnly.html). You can use the following editor configuration: ``` ClassicEditor .create( { commentsOnly: true, // More editor's configuration. // ... } ) .then( /* ... */ ) .catch( /* ... */ ); ``` You can also change the mode by setting the [CommentsOnly#isEnabled](../../../api/module%5Fcomments%5Fcommentsonly-CommentsOnly.html#member-isEnabled) property: ``` editor.plugins.get( 'CommentsOnly' ).isEnabled = true; ``` ### Demo Check out the comments-only mode in action in the editor below. Please note how only the content-editing features become disabled in this mode. Other features that do not affect the content like export to Word or search (but not replace!) are still available. ### Security Please remember that your application should be secured both on the frontend and backend. Even though the users will not be able to introduce their changes through the editor, you should still take care of preventing such action in your backend code. source file: "ckeditor5/latest/features/collaboration/comments/comments-outside-editor.html" ## Comments outside the editor The [comments feature API](../../../api/comments.html), together with [Context](../../../api/module%5Fcore%5Fcontext-Context.html), lets you create deeper integrations with your application. One such integration is enabling comments on non-editor form fields. In this guide, you will learn how to add this functionality to your application. Additionally, all users connected to the form will be visible in the presence list. ### Before you start For the purposes of this guide, the CKEditor Cloud Services and the [real-time collaborative comments](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) will be used. However, the comments feature API can also be used in a similar way together with [standalone comments](#ckeditor5/latest/features/collaboration/comments/comments-integration.html). ### Preparing the context The goal is to enable comments on non-editor form fields, so we will need to use the context to initialize the comments feature without using the editor. First, you need to prepare [Context](../../../api/module%5Fcore%5Fcontext-Context.html) configuration. You can refer to the [Context and collaboration features](#ckeditor5/latest/features/collaboration/context-and-collaboration-features.html) guide for more in-depth explanation: ### Preparing the HTML structure When the context config is ready, it is time to prepare an HTML structure with an example form, a presence list, and a sidebar. ```
``` The form contains several fields, as shown above. Each field has a button that allows for creating a comment attached to that field. Each field is assigned a unique ID. Also, the `tabindex="-1"` attribute was added to make it possible to focus the DOM elements (and add them to the [focus trackers](../../../api/module%5Futils%5Ffocustracker-FocusTracker.html)). Then, style HTML as below. ``` #editor-presence { width: 679px; margin: 0 auto; } #container { display: flex; position: relative; width: 679px; margin: 0 auto; } #editor-annotations { width: 300px; } .form-field { padding: 8px 10px; margin-bottom: 20px; outline: none; margin-right: 20px; border: 1px solid #DDDDDD; border-radius: 3px; } .form-field.has-comment { background: hsl(55, 98%, 83%); } .form-field.active { background: hsl(55, 98%, 68%); } .form-field label { display: inline-block; width: 100px; } .form-field input, .form-field select { width: 200px; margin: 0px; padding: 0px 8px; height: 29px; background: #FFFFFF; border: 1px solid #DDDDDD; border-radius: 3px; box-sizing: border-box; } .form-field button { width: 29px; margin: 0px; height: 29px; background: #EEEEEE; border: 1px solid #DDDDDD; border-radius: 3px; vertical-align: top; } ```
### Implementing comments on form fields Now, it is time to integrate comments with our custom UI. The integration will meet the following requirements: 1. It will be possible to add a comment thread to any form field. 2. The comments should be sent, received, and handled in real-time. 3. There can be just one comment thread on a non-editor form field. 4. A button click creates a comment thread or activates an existing thread. 5. There should be a visible indication that there is a comment thread on a given field. #### Creating a context First, create a context instance using the `contextConfig` defined earlier. #### Adding a comment thread To create a new comment thread attached to a form field, use [CommentsRepository#openNewCommentThread()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html). ``` Context.create( contextConfig ).then( context => { // ... document.querySelectorAll( '.form-field button' ).forEach( button => { const field = button.parentNode; button.addEventListener( 'click', () => { // Thread ID must be unique. // Use field ID + current date time to generate a unique thread ID. const threadId = field.id + ':' + new Date().getTime(); commentsRepository.openNewCommentThread( { channelId, threadId, target: () => getAnnotationTarget( field, threadId ), // `context` is additional information about what the comment was made on. // It can be left empty but it also can be set to a custom message. // The value is used when the comment is displayed in comments archive. context: { type: 'text', value: getCustomContextMessage( field ) }, // `isResolvable` indicates whether the comment thread can become resolved. // Set this flag to `false` to disable the possibility of resolving given comment thread. // You will still be able to remove the comment thread. isResolvable: true } ); } ); } ); function getCustomContextMessage( field ) { // This function should return the custom context value for given form field. // It will depend on your application. // Below, we assume HTML structure from this sample. return field.previousSibling.innerText + ' ' + field.value; } } ); ``` #### Handling new comment threads Define a callback that will handle comment threads added to the comments repository – both created by the local user and incoming from remote users. For that, use the [CommentsRepository#addCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#event-addCommentThread). Note that the event name includes the context channel ID. Only comments “added to the context” will be handled. ``` Context.create( contextConfig ).then( context => { // ... // This `Map` is used to store all open threads for a given field. // An open thread is a non-resolved, non-removed thread. // Keys are field IDs, and values are arrays with all opened threads on this field. // Since it is possible to create multiple comment threads on the same field, this `Map` // is used to check if a given field has an open thread. const commentThreadsForField = new Map(); commentsRepository.on( 'addCommentThread:' + channelId, ( evt, data ) => { handleNewCommentThread( data.threadId ); }, { priority: 'low' } ); function handleNewCommentThread( threadId ) { // Get the thread instance and the related DOM element using the thread ID. // Note that thread ID format is "fieldId:time". const thread = commentsRepository.getCommentThread( threadId ); const field = document.getElementById( threadId.split( ':' )[ 0 ] ); // If the thread is not attached yet, attach it. // This is the difference between local and remote comments. // Locally created comments are attached in the `openNewCommentThread()` call. // Remotely created comments need to be attached when they are received. if ( !thread.isAttached ) { thread.attachTo( () => thread.isResolved ? null : field ); } // Add a CSS class to the field to show that it has a comment. field.classList.add( 'has-comment' ); // Get all open threads for given field. const openThreads = commentThreadsForField.get( field.id ) || []; // When an annotation is created or reopened we need to bound its focus manager with the field. // Thanks to that, the annotation will be focused whenever the field is focused as well. // However, this can be done only for one annotation, so we do it only if there are no open // annotations for a given field. if ( !openThreads.length ) { const threadView = commentsRepository._threadToController.get( thread ).view; const annotation = annotations.collection.getByInnerView( threadView ); annotation.focusableElements.add( field ); } // Add new thread to open threads list. openThreads.push( thread ); commentThreadsForField.set( field.id, openThreads ); } } ); ``` When the context is initialized, there could already be some comment threads created by remote users and loaded while the editor was initialized. These comments need to be handled as well. ``` for ( const thread of commentsRepository.getCommentThreads( { channelId } ) ) { // Ignore threads that have been already resolved. if ( !thread.isResolved ) { handleNewCommentThread(thread.id); } } ``` #### Handling removed comment threads You should also handle removing comment threads. To provide that, use the [CommentsRepository#removeCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-RemoveCommentThreadEvent.html). Again, note the event name. ``` Context.create( contextConfig ).then( context => { // ... commentsRepository.on( 'removeCommentThread:' + channelId, ( evt, data ) => { handleRemovedCommentThread( data.threadId ); }, { priority: 'low' } ); function handleRemovedCommentThread( threadId ) { // Note that thread ID format is "fieldId:time". const field = document.getElementById( threadId.split( ':' )[ 0 ] ); const openThreads = commentThreadsForField.get( field.id ); const threadIndex = openThreads.findIndex( openThread => openThread.id === threadId ); // Remove this comment thread from the list of open comment threads for given field. openThreads.splice( threadIndex, 1 ); // In `handleNewCommentThread` we bound the first comment thread annotation focus manager with the field. // If we are removing that comment thread, we need to handle field focus as well. // After removing or resolving the first thread you should field focus to the next thread's annotation. if ( threadIndex === 0 ) { const thread = commentsRepository.getCommentThread( threadId ); const threadController = commentsRepository._threadToController.get( thread ); // Remove the old binding between removed annotation and field. if ( threadController ) { const threadView = threadController.view; const annotation = annotations.collection.getByInnerView( threadView ); annotation.focusableElements.remove( field ); } const newActiveThread = commentThreadsForField[ 0 ]; // If there other open threads, bind another annotation to the field. if ( newActiveThread ) { const newThreadView = commentsRepository._threadToController.get( newActiveThread ).view; const newAnnotation = annotations.collection.getByInnerView( newThreadView ); newAnnotation.focusableElements.add( field ); } } // If there are no more active threads the CSS classes should be removed. if ( openThreads.length === 0 ) { field.classList.remove( 'has-comment', 'active' ); } commentThreadsForField.set( field.id, openThreads ); } } ); ``` #### Handling resolved/reopened comment threads Handling the resolving of comment threads is significant to keep your UI up to date. To manage that, use the [CommentsRepository#resolveCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#event-resolveCommentThread) and, when the thread is opened again, [CommentsRepository#reopenCommentThread event](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html#event-reopenCommentThread). As with the previous point, note the event name. After resolving, the comment thread is removed from the sidebar, however, you can still obtain the annotation from the `Annotations#collection` and render it in a custom comments archive UI. ``` Context.create( contextConfig ).then( context => { // ... commentsRepository.on( 'resolveCommentThread:' + channelId, ( evt, { threadId } ) => { handleRemovedCommentThread( threadId ); }, { priority: 'low' } ); commentsRepository.on( 'reopenCommentThread:' + channelId, ( evt, { threadId } ) => { handleNewCommentThread( threadId ); }, { priority: 'low' } ); } ); ``` #### Highlighting an active form field To make the UI more responsive, it is a good idea to highlight the form field corresponding to the active comment. To add this improvement, add a listener to the [CommentsRepository#activeCommentThread](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html) observable property. ``` Context.create( contextConfig ).then( context => { // ... commentsRepository.on( 'change:activeCommentThread', ( evt, propName, activeThread ) => { // When an active comment thread changes, remove the 'active' class from all the fields. document.querySelectorAll( '.form-field.active' ) .forEach( el => el.classList.remove( 'active' ) ); // If `activeThread` is not null, highlight the corresponding form field. // Handle only comments added to the context channel ID. if ( activeThread && activeThread.channelId == channelId ) { const field = document.getElementById( activeThread.id.split( ':' )[ 0 ] ); field.classList.add( 'active' ); } } ); } ); ``` ### Full implementation Below you can find the final solution. This is the content of the `main.js` file: The HTML structure and styles of the `index.html` file: ``` CKEditor5 Collaboration – Hello World!
```
### Demo Click the “plus” button to add a comment. source file: "ckeditor5/latest/features/collaboration/comments/comments-walkthrough.html" ## Comments walkthrough This guide explores various functionalities related to the comments feature. It also explains how the comments feature integrates with other [editor features](#ckeditor5/latest/features/index.html). ### Comments and comment threads Selecting a content fragment and pressing the toolbar button displays an annotation in the editor. This signifies that a comment thread was initiated, however, it does not contain any comments yet. The thread is submitted upon adding the first comment, which happens when you click the “Comment” button. If you click the “Cancel” button, the comment thread will be removed. Comment thread views are also called “annotations” or “balloons.” You can display [annotations](#ckeditor5/latest/features/collaboration/annotations/annotations.html) in one of three different display modes: * A wide sidebar * A narrow sidebar (as shown in this guide) * Inline You can read more about these modes in the [Annotations display mode](#ckeditor5/latest/features/collaboration/annotations/annotations-display-mode.html) guide. ### Comment thread view Each comment in a thread is shown with its author, their picture (avatar), and the comment creation time. The top-right corner of each comment displays actions available based on [user permissions](#ckeditor5/latest/features/collaboration/users.html--user-permissions). The “Resolve” and “Remove” buttons next to the first comment apply to **the entire thread**. Using the “Remove” action for any following comments deletes only the selected comment. You can add custom actions to this dropdown and create other [customizations for the comment thread view](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-view.html). At the bottom of the annotation, there is an input field. Users can type in it to keep the discussion going. The availability of this input depends on user permissions. ### Comment thread states Created comment threads can assume various states over their lifecycle. #### Open This is the default state of a comment thread. The thread is available in the document, with its annotation positioned next to the highlighted content. Users can discuss by adding more comments (replies) to the thread. #### Resolved After clicking the “Resolve” button, the thread becomes resolved. It is moved to the **comments archive**. You can access a resolved thread in a dedicated dropdown . Clicking a resolved thread redirects you to the highlighted content element where the thread was initiated. A resolved comment thread looks different from an open one. There is a yellow bar within the annotation, whose main part is the **context**: * For textual content – This is the text on which the thread was created. * For an image – This will be its alternative text. * For a block element with no textual information – The bar will display the default context information “Comment was made on an element.” You can reopen a resolved comment thread by: * Replying to it in the comments archive. * Clicking the “Reopen” button next to the context. At the end of the comments list, you can see the details of when and by whom the thread was resolved. This information will disappear when you reopen the thread. #### Unlinked When you remove the document content related to a comment thread from the document, the comment thread becomes unlinked. It is then moved to the comments archive. Since it has not been technically resolved, it does not display the usual related information. You can still interact with the unlinked thread inside the comments archive. However, since the related content is removed, it will not be highlighted in the editor. The thread will also not reopen after adding a new comment. You can [resolve and reopen an unlinked thread](#ckeditor5/latest/features/collaboration/comments/comments-walkthrough.html--unlinked-and-resolved) as these two states are handled separately. An unlinked thread will be restored when its related content is restored in the document. This can happen after using undo, restoring an old revision (through the {@features/revision-history revision history feature}), or loading earlier (legacy) document data that contains the related content. #### Unlinked and resolved Threads can also exist in a combined state of being both unlinked and resolved, inheriting the behavior of both states. This means that in such a thread, you can continue the discussion, which will reopen the thread. However, the thread will not show in the document unless you restore the related content (as described above). Since the editor handles these two states separately, a comment thread may become resolved, unlinked, and then reopened and linked in any order. #### Deleted Threads in this state have been permanently removed and you cannot restore them. You can delete a comment thread only using the UI. You must confirm this action before a comment thread is permanently deleted. #### States’ relation and flow If you prefer a visual representation, the following diagram illustrates the various states of a comment thread: ### Integration with other features Certain features integrate with comment threads in non-obvious ways. Such cases are presented in the section below. #### Revision history It is important to understand the difference between unlinked and resolved or deleted comment threads when using [revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history.html). 1. Restoring a revision will reopen an unlinked comment thread. This is because the commented content is available again in the editor. 2. Restoring a revision will not restore a resolved or deleted comment thread. The revision history feature does not restore the comment thread state. This is the behavior expected by the user. When the user resolves or deletes a comment thread, their motivation is that the discussion was completed or was invalid. Even if you bring back the commented content, the discussion is still concluded. The comment thread should therefore stay in the archive (or stay deleted and inaccessible). This is different when a comment thread is archived because its related content is gone. In this case, the discussion is still relevant when the removed content is restored. #### Import from Word The comments feature is also integrated with the [import from Word](#ckeditor5/latest/features/converters/import-word/import-word.html) feature. All comments from imported Word documents are shown as annotations but they are marked as “external.” This aims to differentiate them from comments created inside the editor. It also prevents confusion about the identity of the comment author and clarifies the comment’s origin. You can mark comment threads as resolved in Word. After importing a document, such threads will appear in the editor’s comment archive as resolved. However, the details on who resolved them will not be displayed since this information is unavailable in the Word file. #### Export to Word After exporting the editor content to a `.docx` file using the [export to Word](#ckeditor5/latest/features/converters/export-word.html) feature: * All open and resolved comment threads will be preserved. * Unlinked and deleted threads will not be available. #### Clipboard Comment markers are retained in the clipboard when you cut-and-paste commented content around the document. Similarly, if you drag-and-drop part of the editor content, the comments will be moved together accordingly. This is applied to resolved comment threads as well. Please be aware, that the comment markers will be moved only if the whole comment content was moved. If only a part of a comment was cut-and-pasted or drag-and-dropped, the comment marker will stay on the original content. This was designed to prevent confusion from duplicated comments. If you would like to enable retaining comment markers also for copy-and-paste, you need to add an [appropriate setting](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html#member-copyMarkers) to the editor configuration. source file: "ckeditor5/latest/features/collaboration/comments/comments.html" ## Comments overview The comments feature lets you add comments to any part of your content, including text, and block elements such as embedded media or images. This is a great way of communication for many authors working on the same document. ### Demo Test the comments feature in the editor below. Select a passage or a word you need to comment and use the toolbar button to add a comment. Use the sidebar to check the existing ones, and delete comments or comment threads. You can also resolve threads to declutter the sidebar and find them later in the comment archive dropdown . This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. Commented content is marked as highlighted and a corresponding comment balloon is displayed in the sidebar or inline. Comments can be added, edited, deleted, and replied to, allowing the users to collaborate on the same document directly in the rich text editor. Comments threads can be either deleted or resolved. The latter provides a way to archive comments that are no longer relevant, reducing clutter and making it easier to focus on the most important feedback. Users can access the comments archive from the toolbar and use it to view and restore archived comments if necessary. It helps to simplify the feedback management process. Comments markers are [moved along with the content](#ckeditor5/latest/features/collaboration/comments/comments-walkthrough.html--clipboard) on cut-and-paste and drag-and-drop actions. The comments feature can be used together with [real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) or as a standalone plugin where the comments are saved in your application using a [custom integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html). You can read more about CKEditor 5’s collaboration features and their real-life implementations in this [dedicated blog post](https://ckeditor.com/blog/Feature-of-the-month-Collaborative-writing-in-CKEditor-5/). A [comments-only mode](#ckeditor5/latest/features/collaboration/comments/comments-only-mode.html) is also available if you want to limit the user permissions and only allow them to add comments to the document, but not edit the content directly. ### Integration #### Use as a standalone plugin The comments plugin does not require real-time collaboration to work. If you prefer a more traditional approach to document editing, the comments can be added to CKEditor 5 just like any other plugin. To learn how to integrate comments as a standalone plugin, refer to the [Integrating comments with your application](#ckeditor5/latest/features/collaboration/comments/comments-integration.html) guide. #### Use with real-time collaboration If you are using the real-time collaboration feature, refer to the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) guide. ### Learn more #### Configuration The configuration details for the comments feature can be found in the [CommentsConfig](../../../api/module%5Fcomments%5Fconfig-CommentsConfig.html) API reference. #### Comments with mentions It is possible to configure the [Mentions feature](#ckeditor5/latest/features/mentions.html) to work with the Comments feature. Here you can find a [detailed description of that process](#ckeditor5/latest/features/collaboration/annotations/annotations-custom-configuration.html--comment-editor-configuration). #### Comments markup Comments are always attached to someplace in the document. To make sure that they will not be lost, the comments plugin adds some special markup to the document: * The `` and `` tags are added if the comment starts/ends in text, * otherwise, the following attributes are added to elements: * `data-comment-start-before`, * `data-comment-end-after`, * `data-comment-start-after`, * `data-comment-end-before`. Read more about the [marker-to-data conversion](../../../api/module%5Fengine%5Fconversion%5Fdowncasthelpers-DowncastHelpers.html#function-markerToData) to understand what data you may expect. ##### Examples of the possible markup Comment on text: ```

They are awesome.

``` Comment on an image: ```
Image caption.
``` If your application filters HTML content, for example, to prevent XSS, you need to make sure to leave the comments tags and attributes in place when saving the content in the database. The comment markup is necessary for further editing sessions. If you need to display the document data without the comments, you can simply remove the comments markup from the document data: ```

They are awesome.

``` When launching the editor, though, make sure to include comments markup in the HTML: ```

They are awesome.

```
#### Saving the data with comment highlights By default, the data returned by `editor.getData()` contains the markup for the comments as described above. It does not provide the markup that visually shows the comment highlights in the data (similarly to how they are shown in the editor). It is possible to change the editor output using the `showCommentHighlights` option passed in `editor.getData()`. When set, the editor output will return comments similarly to how they are present inside the editor: ``` editor.getData( { showCommentHighlights: true } ); ``` Will return: ```

Foo bar

``` The [export to PDF feature](#ckeditor5/latest/features/converters/export-pdf.html) can be integrated with the comment highlights as shown below: ``` { exportPdf: { // More configuration of the Export to PDF. // ... dataCallback: editor => editor.getData( { showCommentHighlights: true } ) } } ```
#### Comments attributes The [comments attributes](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-Comment.html#function-setAttribute) are custom data that can be set and used by features built around comments. They may be used to indicate the urgency or severity of comments, categorize them into thematic fields, and so on. You can use attributes to store your feature data with other comment data. ``` comment.setAttribute( 'isImportant', true ); ``` You can group multiple values in an object, using dot notation: ``` comment.setAttribute( 'customData.type', 'image' ); comment.setAttribute( 'customData.src', 'foo.jpg' ); ``` Attributes set on the comment can be accessed through the `attribute` property: ``` const isImportant = comment.attributes.isImportant; const type = comment.attributes.customData.type; ``` You can also observe the `attributes` property or bind other properties to it: ``` myObj.bind( 'customData' ).to( comment, 'attributes', attributes => attributes.customData ); ``` Whenever [setAttribute()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-Comment.html#function-setAttribute) or [removeAttribute()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-Comment.html#function-removeAttribute) is called, the `attributes` property is re-set, and observables are refreshed. Using these fires the `update` method in an adapter. #### Comments thread attributes Similarly, comment thread attributes are custom data that can be set and used by features built around comments. Use it to store your feature data with other comment thread data. You can also group multiple values in an object, using dot notation: ``` commentThread.setAttribute( 'customData.isImportant', true ); ``` Attributes set on the comment can be accessed through the `attribute` property: ``` const isImportant = commentThread.attributes.customData.isImportant; ``` You can also observe the `attributes` property or bind other properties to it: ``` myObj.bind( 'customData' ).to( commentThread, 'attributes', attributes => attributes.customData ); ``` Whenever [setAttribute()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentThread.html#function-setAttribute) or [removeAttribute()](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentThread.html#function-removeAttribute) is called, the `attributes` property is re-set, and observables are refreshed. #### Why comment content is not stored in the document data? Note that the markers only store the comment thread IDs. They do not include any content for security reasons. If you stored the complete comment discussion with the editor content, a malicious user could edit it, including comments written by other authors. It would be difficult to check which changes in the comments were done by the user when saving data. You would need to analyze the whole content of the document and compare it with the previous version. Considering that both content and comments could change at the same time and they are mixed, it would be a hard task to ensure that only authorized changes were introduced. For these reasons, [adapter integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--adapter-integration) is the recommended solution. However, if you want to save your content together with the comments data, check the [simple “load and save” integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html--a-simple-load-and-save-integration) guide which should help you. #### Characters limit For technical reasons, each comment in the thread has a character limit set to 65000 characters. Note that comment content is stored in the HTML format, so the HTML tags (which are invisible to the user) use up some of the character limit. If the limit is exceeded, the user interface will prevent the user from submitting the comment. #### API overview Check the [comments API documentation](../../../api/comments.html) for detailed information about the comments API. Making yourself familiar with the API may help you understand the code snippets. #### Comments annotations customization The comments annotations are highly customizable. Please refer to the [Annotation customization](#ckeditor5/latest/features/collaboration/annotations/annotations.html) guide to learn more. #### Comments-only mode You can run the editor in the [“comments-only” mode](#ckeditor5/latest/features/collaboration/comments/comments-only-mode.html). In this mode, the user can add, edit and remove comments but is unable to edit the document content or change the document data otherwise. #### Comments for multiple editors If your application displays multiple editors on one page, you might want to use the [context feature](#ckeditor5/latest/features/collaboration/context-and-collaboration-features.html). It will, among others, let you display comments from all editors inside one sidebar. #### Comments outside the editor The comments feature can be also used on regular form-fields or HTML elements. Thanks to that, you can allow for commenting on all elements of your application and provide a unified user experience and interface. See the [Comments outside the editor](#ckeditor5/latest/features/collaboration/comments/comments-outside-editor.html) guide to learn more. source file: "ckeditor5/latest/features/collaboration/context-and-collaboration-features.html" ## Using context with collaboration features The [Context class](../../api/module%5Fcore%5Fcontext-Context.html) organizes multiple editors into one environment. This way you can initialize some features not for a single editor but for its context accessible to many editors. ### Introduction to the context #### Collaboration features and multiple editors Most applications use just one editor instance per a web page or a form. In these integrations, the editor is the topmost entity that initializes features and coordinates their work. Collaboration features such as a sidebar or presence list are linked with this single editor instance and only handle the changes happening within this editor instance. It does not create a good user experience for applications that need to present multiple editors on one web page, though. In that case, each editor instance has its own sidebar and presence list. Also, multiple, separate connections need to be handled for data exchange. The [Context class](../../api/module%5Fcore%5Fcontext-Context.html) was introduced to solve such issues. It organizes multiple editors into one environment. Some of the features, instead of being initialized for a singular editor, can be initialized for the whole context. Then, each editor can use the same instance of the feature, which means that there can be one common presence list or sidebar linked with multiple editors. Note that only selected plugins are prepared to work as context plugins. See the API documentation for collaboration features to learn which plugins can be used as context plugins. Additionally, in integrations using [Context](../../api/module%5Fcore%5Fcontext-Context.html) and multiple editor instances, each editor instance will display only “its own” comment threads in the comments archive panel. #### Collaboration features and no editor A context plugin that is added to a context is ready as soon as the context is created. This allows for using the context plugins without creating an editor at all! Thanks to that you can provide features like [comments on non-editor form fields](#ckeditor5/latest/features/collaboration/comments/comments-outside-editor.html) that are no longer just editor features but are deeply integrated with your application. #### Channel ID The channel ID is used to identify a data storage for collaboration features that a given editor or context should connect to. If you are using multiple editors, each of them must use a different channel ID to connect to a specific document. Additionally, the context itself, needs to specify its own, unique channel ID. To set the channel ID, use the [config.collaboration.channelId](../../api/module%5Fcollaboration-core%5Fconfig-RealTimeCollaborationConfig.html#member-channelId) configuration property. See the code snippets below. The channel ID is frequently used as a parameter or data property in the [comments API](../../api/comments.html). If you are preparing a custom integration using the comments API, you can use the channel ID to recognize whether the comment was added to an editor instance or to a context. ### Before you start #### Preparing a custom editor setup with the context The context can be used with both: standalone collaboration features and real-time collaboration features. Below is an example featuring real-time collaboration. To use `Context`, add it to your editor setup: ### Using the context in an integration After your editor setup is ready, you need to configure and initialize the context and the editor. If you use the HTML produced by the [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs), it is enough to update the content of the `.editor-container__editor` container to this: ```
``` We can now setup three editor instances, sharing one context, on these elements: ``` // An example of initialization of the context and multiple editors: Context.create( contextConfig ).then( context => { // After the context is ready, initialize an editor on all elements in the DOM with the `.editor` class: for ( const editorElement of document.querySelectorAll( '.editor' ) ) { // Create current editor's config based on the earlier defined template. const currentEditorConfig = Object.assign( editorConfig, { root: { element: editorElement }, // Pass the context to the editor. context, // Each editor should connect to its document. // Create a unique channel ID for an editor (document). collaboration: { channelId: 'channel-id-' + editorElement.id } } ); DecoupledEditor.create( currentEditorConfig ).then( editor => { // Insert the toolbar of this editor right above its editing area. document.querySelector( '.editor-container__editor' ) .insertBefore( editor.ui.view.toolbar.element, editor.ui.view.editable.element ); // You can do something with the editor instance after it was initialized. // ... } ); } } ); ``` The demo will render three editor instances, one after another. Since they share one context, you will be able to observe that: * The presence list is shared between them (and they use one collaboration session). * The sidebar with comments is also shared between them.
### Context and watchdog Similarly to an editor instance, context can be integrated with a watchdog to handle the errors that may happen in the editor or context features. Please refer to the [watchdog documentation](#ckeditor5/latest/features/watchdog.html--context-watchdog) to learn how to use the context watchdog. ### Minimal implementation Below are code snippets showcasing the use of a context and a watchdog together with real-time collaboration features. The `main.js` file: The HTML file: ``` CKEditor 5 Collaboration – Hello World!

Editor 1

Foo bar baz

Editor 2

Foo bar baz

```
### Demo source file: "ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html" ## Real-time collaboration feature integration This quick start guide will let you set up real-time collaboration inside your editor, including basic integration with other collaboration features. After reading this article, we recommend visiting the “Learn more” sections in our documentation pages for the [comments](#ckeditor5/latest/features/collaboration/comments/comments.html--learn-more), [track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html--learn-more) and [revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--learn-more) features to get more in-depth knowledge about them. ### Sign up to the collaboration service The real-time collaboration feature needs a server to synchronize content between the clients, so to use it, you need to sign up to the collaboration service first. Refer to the [CKEditor Cloud Services Collaboration – Quick Start](#cs/latest/guides/collaboration/quick-start.html) guide for more details. There is also the [Premium features free trial](#trial/latest/index.html) available for testing purposes, that will provide the necessary backend. #### Preparing a custom editor setup To use the real-time collaboration, you need to prepare a custom editor setup with several features enabled. The easiest way to do that is by using the [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs). Pick a preset and start customizing your editor. **In the “Features” section** of the Builder (2nd step), make sure to: * make sure the “real-time” toggle next to the “Collaboration” group is on, * pick the entire “Collaboration” feature group. Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step. #### Setting up a sample project Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository: ``` npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project cd sample-project npm install ``` Then, install the necessary dependencies: ``` npm install ckeditor5 npm install ckeditor5-premium-features ``` This project template uses [Vite](https://vitejs.dev/) under the hood and contains 3 source files that we will use: `index.html`, `style.css`, and `main.js`. It is now the time to use our custom editor setup. **Go to the “Installation” section** of the Builder and copy the generated code snippets to those 3 files. ### Configure and initialize the editor To finalize the editor configuration, you need to take a few additional steps in the `main.js` file: * In the editor configuration (`editorConfig`), you need to fill in the following fields: * The [CKEditor Cloud Services connection data (cloudServices)](../../../api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html), * The [collaboration.channelId](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html--the-channelid-configuration-property) value. * The valid `licenseKey`. Please refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. * This example uses the [watchdog](#ckeditor5/latest/features/watchdog.html) feature that provides an additional layer of protection against data loss in case the editor crashes. Use [Watchdog.create()](../../../api/module%5Fwatchdog%5Feditorwatchdog-EditorWatchdog.html#function-create) instead of `ClassicEditor.create()` to take advantage of this feature, as shown in the example below. ``` const editorConfig = { /* ... */ attachTo: document.querySelector( '#editor' ), licenseKey: '', cloudServices: { tokenUrl: '', webSocketUrl: '' }, collaboration: { channelId: 'your-unique-channel-per-document' }, /* ... */ }; const { EditorWatchdog } = ClassicEditor; const watchdog = new EditorWatchdog( ClassicEditor ); // Replace ClassicEditor.create(editorConfig); with watchdog.create( editorConfig ); ``` Voilà! All users who open this page should be able to collaborate, working on the same rich text document at the same time. #### The `channelId` configuration property The [config.collaboration.channelId](../../../api/module%5Fcollaboration-core%5Fconfig-RealTimeCollaborationConfig.html#member-channelId) configuration property is an important property that controls which editor instances collaborate with one another. All clients created with the same `channelId` will be collaborating on the same content. Each document must have a different `channelId`. This ID is usually the primary key of the document in the database or a unique identifier for a given form field. However, you are free to provide whatever identifier fits your scenario. The `channelId` needs to be unique in a given environment, so if you are using, for instance, staging and production environments, you may use the same channel ID in these. Since the environments are separated, these IDs will not interfere with one another. Since editor content is linked with and only with the channel ID, it is possible to create an application where different views (forms, subpages, etc.) provide various sets of rich-text editable fields and have users collaborate with each other even though they have opened a different form, etc. ### Handling document data All comments, suggestions, and revisions data is saved in CKEditor Cloud Services to make the integration work out of the box. Note that CKEditor Cloud Services does not save or load the document content. Instead, Cloud Services acts as a medium to handle the collaboration process. Content management should be handled by the application integration. #### Data initialization When the first user opens the rich text editor for a certain document ID, their content is sent to CKEditor Cloud Services. In case of the example above it is: ```

Let's edit this together!

``` If you cannot or do not want to set the initial data straight in the HTML, use the [initialData configuration option](../../../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-initialData) instead. Do not use [editor.setData()](../../../api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-setData) (see below). After the first user initializes the document and starts the editing session, every other user that connects to the same document will receive the content from CKEditor Cloud Services (while their local initial data is discarded). From that moment on, every single change to the editor content is passed between Cloud Services and clients and the document for each client is updated. Any conflicting updates are resolved on the fly. This is the reason why you should not use the `editor.setData()` or `editor.data.set()` methods when using the collaboration plugin. These simply overwrite the whole content of the editor (even if the data to set is the same as the current editor data). In real-time collaboration, this behavior can result in overwriting other clients’ local updates. In most cases, using `editor.setData()` and `editor.data.set()` in real-time collaboration is incorrect. If you are sure that you understand and accept the behavior and side effects of setting the editor data this way, you can call `editor.data.set()` with the flag `suppressErrorInCollaboration` set to `true`: ``` editor.data.set( '

Your data

', { suppressErrorInCollaboration: true } ); ``` This will let you overwrite the editor data without throwing an error.
#### Saving data Although CKEditor Cloud Services handles passing data between clients, you are still responsible for saving the final data in your database. The document is stored in the cloud by CKEditor Cloud Services only temporarily, as long as there are connected users. It means that the editor content should be saved in the database on your server before the last user disconnects – otherwise it will be lost. It is recommended to save the data automatically whenever it changes. CKEditor 5 provides two utilities to make your integration simpler. ##### The `cloudDocumentVersion` property When your collaborative users are saving the same document at the same time, there might be a conflict. It might happen that one user will try to save an older document version, overwriting a newer one. To prevent such race conditions, use the `cloudDocumentVersion` property. ``` editor.plugins.get( 'RealTimeCollaborationClient' ).cloudDocumentVersion ``` This property is simply a number, and a bigger value means a newer document version. This number should be stored in the database together with the document content. When a client wants to save the content, document versions should be compared. The document should only be saved when the version is higher. Otherwise, it means that the incoming data is an older version of the document and should be discarded. CKEditor Cloud Services keeps the value of `cloudDocumentVersion` and makes sure that it is properly set whenever there is a new editing session for a given document. ##### The `Autosave` plugin The second helper is the [Autosave](../../../api/module%5Fautosave%5Fautosave-Autosave.html) plugin. The autosave plugin triggers the save callback whenever the user changes the content. It takes care of throttling the callback execution to limit the number of calls to the database. It also automatically secures the user from leaving the page before the content is saved. The autosave plugin is not enabler by default. To learn more about this plugin, refer to the [Autosave](#ckeditor5/latest/features/autosave.html) feature guide. ##### Autosave for revision history If you are using the revision history feature, your autosave callback should take care of updating the recent revision data. You can add the [autosave](../../../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-autosave) configuration to `editorConfig` in the `main.js` file as shown below: ``` const editorConfig = { /* ... */ autosave: { save: async editor => { const revisionTracker = editor.plugins.get( 'RevisionTracker' ); const currentRevision = revisionTracker.currentRevision; const oldRevisionVersion = currentRevision.toVersion; // Update the current revision with the newest document changes. await revisionTracker.update(); // Check if the revision was updated. // If not, do not make an unnecessary call. if ( oldRevisionVersion === currentRevision.toVersion ) { return true; } // Use the document data saved with the revision instead of the editor data. // Revision data may slightly differ from the editor data when // real-time collaboration is involved. const documentData = await revisionTracker.getRevisionDocumentData( revisionTracker.currentRevision ); // Use revision version instead of `cloudDocumentVersion`. const documentVersion = currentRevision.toVersion; // `saveData()` should save the document in your database. // `documentData` contains data for all roots. // You can save just `documentData.main` if you are using a single root, // or the whole object if you are using multiple roots. return saveData( documentData.main, documentVersion ); } }, /* ... */ } ``` You can read more about saving revisions in the [Revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--how-revisions-are-updated-and-saved) guide. ### Reconnection process During the real-time collaboration, it may happen that the internet connection will go down for some time. When this happens, the editor will switch into read-only mode. After the connection is back, the real-time collaboration plugin will try to reconnect the editor back to the editing session. This reconnection process may involve re-creating the editing session (if the reconnection happens after a longer period of time). While one client is offline, other clients may perform changes to the document. When the offline client reconnects, the real-time editing plugin will make a decision if the reconnection is possible. ### Users selection The real-time collaborative editing feature not only synchronizes the document content between all participants, but it also shows each user the list of all other users in real time. It works out of the box and does not require any additional configuration. This is the only part of the real-time collaborative editing plugin that provides a UI. Refer to the [Users in real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html) guide to learn more. ### Real-time collaboration samples Please visit the [ckeditor5-collaboration-samples](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master) GitHub repository to find various sample integrations of the real-time collaboration feature. source file: "ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html" ## Real-time collaboration overview The real-time collaboration features let many users simultaneously edit content, leave comments, suggest changes, and access revision history. You can include these features in any CKEditor 5 preset and tailor them to your needs. ### Demo Try CKEditor 5 collaboration features in the demo below. Use the comments, track changes, and revision history toolbar items to enable each feature. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. The CKEditor 5 real-time collaboration experience is enabled by several plugins that can be added to the editor preset just like any other CKEditor 5 plugin. These include two base, real-time-collaboration-oriented features: * Real-time collaborative editing – Allows for editing the same document by multiple users simultaneously. It also automatically solves all conflicts if users make changes at the same time. * [Users selection and presence list](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html) – Shows the selection of other users and lets you view the list of users currently editing the content in the editor. We also provide plugins that integrate our other premium collaboration features with real-time editing: * Real-time collaborative [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) – Makes it possible to add comments to any part of content in the editor. * Real-time collaborative [track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) – Changes to the content are saved as suggestions that can be accepted or discarded later. * Real-time [revision history](#ckeditor5/latest/features/collaboration/revision-history/revision-history.html) – Create and view the chronological revision history of the document. Refer to the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) guide to learn how to enable real-time collaboration in your WYSIWYG editor. You can read more about CKEditor 5’s collaboration features and their real-life implementations in this [dedicated blog post](https://ckeditor.com/blog/Feature-of-the-month-Collaborative-writing-in-CKEditor-5/). source file: "ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html" ## Users in real-time collaboration After you [enable the real-time collaborative editing](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) plugin, the selections of collaborating users will automatically display to all of them with no additional configuration. The editor will also set the proper permissions for the local user based on data received from the server. There are even more user features that can be enabled in real-time collaboration. The package contains an easy-to-install plugin to show the list of connected users and the sessions API to observe this collection. ### User roles and permissions CKEditor 5 supports setting user roles and permissions to enable/disable some editor functionalities for the user. To learn how to define user roles and permissions, refer to the [Roles and permissions](#cs/latest/developer-resources/security/roles.html) guide of the Cloud Services documentation. ### Users presence list The [PresenceList](../../../api/module%5Freal-time-collaboration%5Fpresencelist-PresenceList.html) plugin provides a UI which displays all users that are currently connected to the edited document. The users are displayed as a row of avatars. The information about the users is provided by [CKEditor Cloud Services](#cs/latest/developer-resources/security/token-endpoint.html--user). The presence list UI collapses to a dropdown if six or more users are connected. By default, the local user avatar is always present on the list. You can hide it by setting the [displayMe](../../../api/module%5Freal-time-collaboration%5Fconfig-RtcPresenceListConfig.html#member-displayMe) configuration flag to `false`. It is also mark with an outline and listed first. #### Installation and configuration The [plugin configuration](../../../api/module%5Freal-time-collaboration%5Fconfig-RtcPresenceListConfig.html) consists of three options: * `container` – This is a DOM element that will hold the feature’s UI. It is required and already defined if you use the code from the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) tutorial. * `collapseAt` – This optional parameter defines how many users need to be connected to switch the presence list to the dropdown view. The default value is `6`. * `onClick` – Here, you can pass a callback function that will be invoked after a click in a presence list member. This function is invoked with two arguments: the `user` and the `element`. The first provides the clicked member details, and the second is the clicked element. This option is not required. You can add the following code to the editor configuration object to get more control over the user presence list: ``` const editorConfig = { /* ... */ presenceList: { // Existing configuration. container: document.querySelector('#editor-presence'), // Additional configuration. collapseAt: 3, onClick: ( user, element ) => console.log( user, element ) }, /* ... */ } ``` #### Theme customization Like in the whole CKEditor 5 Ecosystem [PostCSS](http://postcss.org) is used to handle styles with the power of [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fvariables). The user presence list feature also uses it to make it possible to easily customize its appearance. By default a presence list has two states with a dedicated design: * Avatars displayed inline in the container (with fewer than 5 users). * A dropdown panel for more than 5 users connected. #### Example of presence list customization with CSS Variables With [inheritance of CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fvariables#Inheritance%5Fof%5FCSS%5FVariables) you can change the default values of variables. You can override these properties with a `.css` file or place your customizations directly into the `` section of your page, but in this case, you will need to use a more specific CSS selector than `:root` (like ``). Add the following styles to the `style.css` file created in the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) guide: ``` /* Change the user presence list hue to greenish. */ .ck.ck-presence-list { --ck-user-avatar-background: #215a11; } .ck.ck-presence-list__balloon { /* Make a smaller user avatar in the dropdown list. */ --ck-user-avatar-size: 25px; --ck-user-avatar-background: #215a11; --ck-color-presence-list-dropdown-background: #d0ecd2; --ck-color-presence-list-dropdown-arrow-border: #d0ecd2; } .ck.ck-presence-list__balloon .ck.ck-presence-list__dropdown-list-wrapper { /* Reduce the minimum and maximum width of the dropdown. */ --ck-presence-list-dropdown-list-min-width: 100px; --ck-presence-list-dropdown-list-max-width: 150px; } ``` To change the color assigned to users, refer to the [Users API](#ckeditor5/latest/features/collaboration/users.html--theme-customization) guide. The examples above will generate the following presence list designs: ### Sessions The [Sessions](../../../api/module%5Freal-time-collaboration%5Frealtimecollaborativeediting%5Fsessions-Sessions.html) plugin stores information about users and sessions connected to the rich text editor. You may say that the presence list is a visualization of the sessions plugin data. The difference between the sessions plugin and the [users plugin](#ckeditor5/latest/features/collaboration/users.html) is that the latter also keeps information about the users who are not currently connected to the editor (for example, a comment author who is currently offline). If your integration uses the [context feature](#ckeditor5/latest/features/collaboration/context-and-collaboration-features.html) and there are multiple channels used, the `Sessions` plugin aggregates users connected to all the channels. There are two types of entries in the sessions plugin: connected users and sessions. There is one session for each connection to a given channel. For example, for each open editor instance connecting to a given channel ID, there will be a session. Every session has a user. However, the same user can be linked with multiple sessions (for example, the same user opened the same URL in multiple tabs). In other words, if the same user (with the same user ID) opens the same document in two different tabs, they will create two sessions but only one user will be connected. You will be able to see two selections in the same document, both in the same color, but only a single user in the [user presence list](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html--users-presence-list). #### Sessions API If you use real-time collaboration features, the `Sessions` plugin will be loaded and available: ``` const sessionsPlugin = editor.plugins.get( 'Sessions' ); ``` Check the [API of the Session plugin](../../../api/module%5Freal-time-collaboration%5Frealtimecollaborativeediting%5Fsessions-Sessions.html) to learn how to use it. source file: "ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html" ## Integrating revision history with your application The revision history plugin [provides an API](../../../api/revision-history.html) that lets you create and manage named revisions of your document. To save and access revisions in your database, you first need to integrate this feature. ### Integration methods This guide will discuss two ways to integrate CKEditor 5 with the revision history plugin: * [A simple “load and save” integration](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--a-simple-load-and-save-integration) using the `RevisionHistory` plugin API. * [An adapter integration](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--adapter-integration) which saves the revision data immediately in the database. The adapter integration is the recommended approach for two reasons: * It gives you better control over the data. * It is more efficient and provides a better user experience, as the revisions’ data is loaded on demand rather than upfront before the editor is initialized. ### Before you start #### Preparing a custom editor setup To use the revision history plugin, you need to prepare a custom editor setup with the asynchronous version of the revision history feature included. The easiest way to do that is by using the [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs). Pick a preset and start customizing your editor. **In the “Features” section** of the Builder (2nd step), make sure to: * enable the “Collaboration → Revision History” feature, * turn off the “real-time” toggle next to the “Collaboration” group. Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step. #### Setting up a sample project Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository: ``` npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project cd sample-project npm install ``` Then, install the necessary dependencies: ``` npm install ckeditor5 npm install ckeditor5-premium-features ``` This project template uses [Vite](https://vitejs.dev/) under the hood and contains 3 source files that we will use: `index.html`, `style.css`, and `main.js`. It is now the time to use our custom editor setup. **Go to the “Installation” section** of the Builder and copy the generated code snippets to those 3 files. #### Activating the feature To use this premium feature, you need to activate it with a license key. Refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. After you have successfully obtained the license key open the `index.js` file and update the `your-license-key` string with your license key. #### Building the project Finally, build the project by running: ``` npm run dev ``` When you open the sample in the browser you should see the WYSIWYG editor with the revision history plugin. However, it still does not load or save any data. You will learn how to add data to the comments plugin later in this guide. Let’s now dive deeper into the structure of this setup. #### Basic setup’s anatomy Let’s now go through the key fragments of this basic setup. ##### HTML structure The HTML and CSS structure of the page creates two main containers: * `
` is the main container used by the editor. * `
` is the main container used by revision history. * `
` is the container used by revision history that shows selected revision content. * `
` is the container used by revision history sidebar that holds revision list. ##### JavaScript The `main.js` file sets up the editor instance: * Adds the `revisionHistory` button to the editor toolbar. * Loads all necessary editor plugins (including the [RevisionHistory](../../../api/module%5Frevision-history%5Frevisionhistory-RevisionHistory.html) plugin). * Sets the `licenseKey` configuration option. * Sets the `revisionHistory` configuration option to point to containers mentioned above. * Defines the templates for the `RevisionHistoryIntegration` and `UsersIntegrations` plugins that we will use in the next steps of this tutorial. #### Revision history API The integration below uses the revision history API. Making yourself familiar with the API may help you understand the code snippets. In case of any problems, refer to the [revision history API documentation](../../../api/revision-history.html). #### Next steps We have set up a simple JavaScript project that runs a basic CKEditor instance with the asynchronous version of the Revision history feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods. ### A simple “load and save” integration In this solution, you load the user and revision data during the editor initialization. You save the revision data after you finish working with the editor (for example, when you submit the form containing the WYSIWYG editor). #### Loading the data After you include the revision history plugin in the editor, you need to create a plugin that will initialize users and existing revisions. First, store the users and the revisions data into a variable that will be available for your plugin. ``` // Revisions data will be available under a global variable `revisions`. const revisions = [ { "id": "initial", "name": "Initial revision", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]' } }, "createdAt": "2024-05-27T13:22:59.077Z", "attributes": {}, "fromVersion": 1, "toVersion": 1 }, { "id": "e6f80e6be6ee6057fd5a449ab13fba25d", "name": "Updated with the actual data", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]' } }, "createdAt": "2024-05-27T13:23:52.553Z", "attributes": {}, "fromVersion": 1, "toVersion": 20 }, { "id": "e6590c50ccbc86acacb7d27231ad32064", "name": "Inserted logo", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]' } }, "createdAt": "2024-05-27T13:26:39.252Z", "attributes": {}, "fromVersion": 20, "toVersion": 24 }, // An empty current revision. { "id": "egh91t5jccbi894cacxx7dz7t36aj3k021", "name": null, "creatorId": null, "authorsIds": [], "diffData": { "main": { "insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]' } }, "createdAt": "2024-05-27T13:26:39.252Z", "attributes": {}, "fromVersion": 24, "toVersion": 24 } ]; ``` Then, modify `RevisionHistoryIntegration` plugin to read the data from `revisions` array and load it to editor using `RevisionsRepository` API. ``` class RevisionHistoryIntegration extends Plugin { static get pluginName() { return 'RevisionHistoryIntegration'; } static get requires() { return [ 'RevisionHistory' ]; } init() { const revisionHistory = this.editor.plugins.get( 'RevisionHistory' ); for ( const revisionData of revisions ) { revisionHistory.addRevisionData( revisionData ); } } } ``` Since revision data will be used, `editorConfig.root.initialData` is no longer needed and you can remove it as described in [Editor initial data and revision data](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--editor-initial-data-and-revision-data). #### Saving the data To save the revisions data, you need to get it from the `RevisionsRepository` plugin first. To do this, use the `getRevisions()` method. Then, use the data to save it in your database in a selected way. See the example below. Remember to update your HTML structure to contain a button with the `get-data` ID, for example, ``. ``` ClassicEditor .create({ ...editorConfig, attachTo: document.querySelector('#editor'), }) .then( editor => { // After the editor is initialized, add an action to be performed after a button is clicked. document.querySelector( '#get-data' ).addEventListener( 'click', () => { const revisionHistory = editor.plugins.get( 'RevisionHistory' ); // Get the document data and the revisions data (in JSON format, so it is easier to save). const editorData = editor.data.get(); const revisionsData = revisionHistory.getRevisions( { toJSON: true } ); // Now, use `editorData` and `revisionsData` to save the data in your application. // // Note: it is a good idea to verify the revision `creatorId` parameter when saving // a revision in the database. However, do not overwrite the value if it was set to `null`! console.log( editorData ); console.log( revisionsData ); } ); } ) .catch( error => console.error( error ) ); ``` The integration is now ready to use with your rich text editor. #### Demo ##### Console ``` ``` ### Adapter integration Adapter integration uses an adapter object – provided by you – to immediately save revisions data in your data store. This is the recommended way of integrating revision history with your application as it lets you handle the client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side. Additionally, revisions may include a significant amount of data. Loading multiple revisions of a big document may adversely impact the loading time of your application. When using an adapter, the revision data is loaded on demand, when needed. This improves the overall user experience. It is important to load the revisions data during the plugin initialization step, that is, using the `init()` method of the integration plugin that you will provide (as in the example below). #### Saving document data and revisions data Before you move to the actual implementation remember that you should **always keep the document data and revisions data in sync**. This means that whenever you save one, you should save the other as well. This is natural for the “load & save” integration but for adapter integration, you need to keep that in mind. A mismatch in the data will result in the feature not working correctly. Additionally, the document data should not be further post-processed after it is saved. To be precise, it should not be changed in a way that would result in a different model after you load the document data. #### Implementation First, define the adapter using the [RevisionHistory#adapter](../../../api/module%5Frevision-history%5Frevisionhistory-RevisionHistory.html#member-adapter) property. The adapter methods allow you to load and save changes in your database. Read the API reference for [RevisionHistoryAdapter](../../../api/module%5Frevision-history%5Frevisionhistoryadapter-RevisionHistoryAdapter.html) carefully to make sure that you integrate the feature with your application correctly. Each change in revisions is performed immediately on the UI side. However, all adapter actions are asynchronous and are performed in the background. Because of this, all adapter methods need to return a `Promise`. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor throws a [CKEditorError](../../../api/module%5Futils%5Fckeditorerror-CKEditorError.html) error, which works nicely together with the [watchdog](#ckeditor5/latest/features/watchdog.html) feature. When you handle the server response, you can decide if the promise should be resolved or rejected. While the adapter is saving the revision’s data, a pending action is automatically added to the editor [PendingActions](../../../api/module%5Fcore%5Fpendingactions-PendingActions.html) plugin. Thanks to this, you do not have to worry that the editor will be destroyed before the adapter action has finished. Now you are ready to create the adapter. Let’s start with mocking revisions data. In real-life scenario this will be the data stored in your data storage. ``` // Revisions data will be available under a global variable `revisions`. const revisions = [ { "id": "initial", "name": "Initial revision", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]' } }, "createdAt": "2024-05-27T13:22:59.077Z", "attributes": {}, "fromVersion": 1, "toVersion": 1 }, { "id": "e6f80e6be6ee6057fd5a449ab13fba25d", "name": "Updated with the actual data", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]' } }, "createdAt": "2024-05-27T13:23:52.553Z", "attributes": {}, "fromVersion": 1, "toVersion": 20 }, { "id": "e6590c50ccbc86acacb7d27231ad32064", "name": "Inserted logo", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]' } }, "createdAt": "2024-05-27T13:26:39.252Z", "attributes": {}, "fromVersion": 20, "toVersion": 24 }, // An empty current revision. { "id": "egh91t5jccbi894cacxx7dz7t36aj3k021", "name": null, "creatorId": null, "authorsIds": [], "diffData": { "main": { "insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]', "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]' } }, "createdAt": "2024-05-27T13:26:39.252Z", "attributes": {}, "fromVersion": 24, "toVersion": 24 } ]; ``` Next, create adapter plugin based on `RevisionHistoryIntegration` plugin template to fetch revisions data asynchronously. ``` // A plugin that introduces the adapter. class RevisionHistoryIntegration extends Plugin { static get pluginName() { return 'RevisionHistoryIntegration'; } static get requires() { return [ 'RevisionHistory' ]; } async init() { const revisionHistory = this.editor.plugins.get( 'RevisionHistory' ); revisionHistory.adapter = { getRevision: ( { revisionId } ) => { return this._findRevision( revisionId ); }, updateRevisions: revisionsData => { const documentData = this.editor.getData(); // This should be an asynchronous request to your database // that saves `revisionsData` and `documentData`. // // The document data should be saved each time a revision is saved. // // `revisionsData` is an array with objects, // where each object contains updated and new revisions. // // See the API reference for `RevisionHistoryAdapter` to learn // how to correctly integrate the feature with your application. // return Promise.resolve(); } }; // Add the revisions data for existing revisions. const revisionsData = await this._fetchRevisionsData(); for ( const revisionData of revisionsData ) { revisionHistory.addRevisionData( revisionData ); } } async _findRevision( revisionId ) { // Get the revision data based on its ID. // This should be an asynchronous request to your database. return Promise.resolve( revisions.find( revision => revision.id === revisionId ) ); } async _fetchRevisionsData() { // Get a list of all revisions. // This should be an asynchronous call to your database. // // Note that the revision list should not contain the `diffData` property. // The `diffData` property may be big and will be fetched on demand by `adapter.getRevision()`. return Promise.resolve(revisions.map(revision => ({ ...revision, diffData: undefined }))); } } ``` Since revision data will be used, `editorConfig.root.initialData` is no longer needed and you can remove it as described in [Editor initial data and revision data](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html--editor-initial-data-and-revision-data). The adapter is now ready to use with your rich text editor. #### Demo ##### Revision history adapter actions console ``` ``` ### Learn more #### Pending actions Revision history uses the [pending actions](../../../api/module%5Fcore%5Fpendingactions-PendingActions.html) feature. Pending actions are added when the revisions data is being updated through the revision history adapter, to prevent closing the editor before the update finishes. #### Editor initial data and revision data When the revision history feature is used, it is crucial to keep the revisions state in synchronization with the document data. Because of that, if there are any revisions saved for a document, the editor initial data will be discarded. The data saved with the most recent revision will be used instead. If the editor’s initial data was set and it was different from the revision data, a warning will be logged in the console. #### Autosave integration This section describes how to integrate revision history with the autosave plugin. This way, you can frequently save your document and revision data to keep them in sync. The integration differs a bit whether you use the adapter or not. ##### Autosave and “load & save” integration Update the revision and make sure that the updated or created revision is saved together with the editor data: ``` autosave: { save: async editor => { const revisionTracker = editor.plugins.get( 'RevisionTracker' ); await revisionTracker.update(); const revisionData = revisionTracker.currentRevision.toJSON(); const documentData = editor.getData(); // `saveData()` should save the document and revision data in your database // and return a `Promise` that resolves when the save is completed. return saveData( documentData, revisionData ); } } ``` ##### Autosave and adapter integration Integration when using the adapter is easier. Your revision adapter should save the document data as well, as was already shown in the earlier examples. Since the adapter already takes care of saving both revision data and the document data, all that needs to be done in the autosave integration is to update the revision: ``` autosave: { save: editor => { const revisionTracker = editor.plugins.get( 'RevisionTracker' ); return revisionTracker.update(); } } ``` ##### Advanced autosave strategies Presented integrations will keep on updating the same revision until the user explicitly saves or names the current revision, or closes the editor. This may result in creating a big revision, containing a lot of changes. To prevent that, autosave integration could create new revisions based on your custom strategy. For example, you may decide to save the current revision (unsaved changes) after a chosen number of autosave callbacks since the last saved revision: ``` // Create a new plugin that will handle the autosave logic. class RevisionHistoryAutosaveIntegration extends Plugin { init() { this._saveAfter = 100; // Create a new revision after 100 saves. this._autosaveCount = 1; // Current autosave counter. this._lastCreatedAt = null; // Revision `createdAt` value, when the revision was last autosaved. } async autosave() { const revisionTracker = this.editor.plugins.get( 'RevisionTracker' ); const currentRevision = revisionTracker.currentRevision; if ( currentRevision.createdAt > this._lastCreatedAt ) { // Revision was saved or updated in the meantime by a different source (not autosave). // Reset the counter. this._autosaveCount = 1; } if ( this._autosaveCount === this._saveAfter ) { // We reached the count. Save all changes as a new revision. Reset the counter. await revisionTracker.saveRevision(); this._autosaveCount = 1; this._lastCreatedAt = currentRevision.createdAt; } else { // Try updating the "current revision" with the new document changes. // If there are any new changes, the `createdAt` property will change its value. // Do not raise the counter, if the revision has not been updated! await revisionTracker.update(); if ( currentRevision.createdAt > this._lastCreatedAt ) { this._autosaveCount++; this._lastCreatedAt = currentRevision.createdAt; } } return true; } } ClassicEditor .create( { attachTo: document.querySelector( '#editor' ), extraPlugins: [ // ... // Add the new plugin to the editor configuration: RevisionHistoryAutosaveIntegration ], // ... // Add the autosave configuration -- call the plugin method: autosave: { save: editor => { return editor.plugins.get( RevisionHistoryAutosaveIntegration ).autosave(); } } } ) .catch( error => console.error( error ) ); ``` Similarly, you can implement a saving strategy that would include time since the last saved revision, number of operations, or multiple variables used together to decide if a new revision should be saved. #### How revisions are updated and saved Understanding how and when revisions are updated and saved is important when it comes to writing custom code that integrates with revision history, including autosave integration. There are always at least two revisions available for a document: the initial revision and the current revision. If the document is new and no revisions have been created for it yet, the two revisions are created when the editor is initialized. The **initial revision** contains the editor data from when the document was initialized for the first time. It can be empty or contain some content. The initial revision’s ID will equal to document ID or to `'initial'` if the document ID is not specified. The **current revision** is a revision that stores all unsaved document changes, that is, changes that have not been saved in earlier revisions. It is always at the top of the revisions list. If a new revision is created, it will contain all unsaved changes and will be added below the current revision. Then, the current revision will be empty, until again updated with new, unsaved document changes. An empty current revision is not shown on the revisions list. The current revision is not updated automatically when the document changes. The update can be done using the revisions feature API. It is enough to update the current revision when you need to save it, for example, in the autosave callback. The update is also triggered when the revision history view is opened. In this case, either the autosave callback is called, or the current revision is updated (if the autosave plugin is not used). A new revision can be saved using the revisions feature API. Also, a new revision will be created when: * A user saves a revision using the dropdown in the editor toolbar. * A user gives a name to the current revision (in the revision history view). * Each time the editor is initialized (a new current revision will be created, while the old current revision will become a regular revision). ##### Save or update revision using the feature API ``` const revisionTrackerPlugin = this.editor.plugins.get( 'RevisionTracker' ); // Updates the "current revision", that is, the revision containing unsaved changes. revisionTrackerPlugin.update(); // Creates a new revision that will contain all the unsaved changes. // See the API reference to learn more. revisionTrackerPlugin.saveRevision(); revisionTrackerPlugin.saveRevision( { name: 'My revision' } ); ``` ### Revision history samples Please visit the [ckeditor5-collaboration-samples](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master) GitHub repository to find several sample integrations of the revision history feature. source file: "ckeditor5/latest/features/collaboration/revision-history/revision-history.html" ## Revision history overview The revision history feature is a document versioning tool that shows you how your content has changed over time. It lets you review both content changes and suggestions made with the track changes feature. ### Demo Use the editor below to test document versioning. Introduce changes, then use the revision history toolbar button to gain access to the revision tools. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Additional feature information With the revision history feature, you can: * Create a named historical version – a revision – of the content by bundling changes together in a save cycle. * View the chronological history of revisions of your content. * Restore an earlier revision of your document to reverse any changes. * Compare selected revisions to each other and view changes between. You can also read a dedicated blog post that [compares revision history with the track changes feature](https://ckeditor.com/blog/ckeditor-5-comparing-revision-history-with-track-changes/) and points out similarities and differences between these two features. ### Use as a standalone plugin Revision history does not require real-time collaboration to work. You can add it to CKEditor 5 just like any other plugin. To learn how to integrate revision history as a standalone plugin, refer to the [Integrating revision history with your application](#ckeditor5/latest/features/collaboration/revision-history/revision-history-integration.html) guide. ### Use with real-time collaboration If you are using the real-time collaboration feature, refer to the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) guide. source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes-custom-features.html" ## Integrating track changes with custom features The track changes feature was designed with custom features in mind. In this guide, you will learn how to integrate your plugins with the track changes feature. ### Enabling commands CKEditor 5 uses [commands](#ckeditor5/latest/framework/architecture/core-editor-architecture.html--commands) to change the editor content. Most modifications of the editor content are done through command execution. Also, commands are usually connected with toolbar buttons that represent their state. For example, if a given command is disabled at the moment, the toolbar button is disabled as well. By default, a command is disabled when tracking changes is turned on. This is to prevent errors and incorrect behavior of a command that has not been prepared to work in the suggestion mode. The first step to integrating your command is to enable it: ``` const trackChangesEditing = editor.plugins.get( 'TrackChangesEditing' ); trackChangesEditing.enableCommand( 'commandName' ); ``` Now the command will be enabled in the track changes mode. However, this on its own does not introduce any additional functionality related to integration with the track changes feature. The command will work as though track changes is turned off. If your command does not introduce any change to the document model (for example, it shows a UI component), it may be enough to just simply enable it. Further integration steps will depend on what kind of change your command performs. ### Insertions and deletions Many custom features are focused on introducing a new kind of element or a widget. It is recommended to use [model.insertContent()](../../../api/module%5Fengine%5Fmodel%5Fmodel-Model.html#function-insertContent) to insert such object into the document content. This method, apart from taking care of maintaining a proper document structure, is also integrated with the track changes feature. Any content inserted with this method will be automatically marked with an insertion suggestion. Similarly, removing is also integrated with track changes through the integration with [model.deleteContent()](../../../api/module%5Fengine%5Fmodel%5Fmodel-Model.html#function-deleteContent). Calling this method while in the suggestion mode will create a deletion suggestion instead. The same goes for actions that remove content, for example using the Backspace and Delete keys, typing over selected content, or pasting over the selected content. In summary, if your command inserts or removes a widget, chances are it will work in the suggestion mode out-of-the-box. One thing that may still need your attention is suggestion description. To generate a proper suggestion description, you need to [register a label for the introduced element](../../../api/module%5Ftrack-changes%5Fsuggestiondescriptionfactory-SuggestionDescriptionFactory.html#function-registerElementLabel). The description mechanism supports translations and different labels for single and multiple added/removed elements. An example of integration for the page break element: ``` const t = editor.t; trackChangesEditing.descriptionFactory.registerElementLabel( 'pageBreak', quantity => t( { string: 'page break', plural: '%0 page breaks', id: 'ELEMENT_PAGE_BREAK' }, quantity ) ); ``` After registering an element label, it will be used in suggestion descriptions. For the above example, the description may be “**Remove:** page break” or, for example, “**Insert:** 3 page breaks.” Using the translation system and adding translations for custom features is described in the [Localization](#ckeditor5/latest/framework/deep-dive/ui/localization.html) guide. If you do not need to support multiple languages, you can skip using translations system: ``` trackChangesEditing.descriptionFactory.registerElementLabel( 'customElement', quantity => quantity == 1 ? 'custom element' : quantity + ' custom elements' ); ``` If you do not specify a label, then the model element is used by default. If for some reason your feature cannot use `model.insertContent()` and `model.deleteContent()`, or you need a more advanced integration, you can assign a custom callback that will be called whenever the command is executed while suggestion mode is on: ``` trackChangesEditing.enableCommand( 'commandName', ( executeCommand, options ) => { // Here you can overwrite what happens when the command is executed in the suggestion mode. // See the API documentation to learn more about the parameters passed to the callback. // ... } ); ``` Then you will need to use one of the following methods from the track changes API: * [markInsertion()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markInsertion) – Creates an insertion suggestion on a given range. * [markMultiRangeInsertion()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markMultiRangeInsertion) – Creates one insertion suggestion that contains multiple ranges. * [markDeletion()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markDeletion) – Creates a deletion suggestion on a given range. * [markMultiRangeDeletion()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markMultiRangeDeletion) – Creates one deletion suggestion that contains multiple ranges. ### Attribute changes Another category of changes is attribute changes. If your custom feature introduces attribute that is applied to text or can be changed by the user, you may want to implement this type of integration. To enable tracking attribute changes, use [enableDefaultAttributesIntegration()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-enableDefaultAttributesIntegration) method. It is a simple helper, designed to handle most of the scenarios related to attribute changes. It will enable the command, so you do not need to additionally use `enableCommand()`. Additionally, you will need to register every attribute key that you will want to track. This is done using [SuggestionsConversion#registerInlineAttribute()](../../../api/module%5Fcollaboration-core%5Fsuggestions%5Fsuggestionsconversion-SuggestionsConversion.html#function-registerInlineAttribute) and [SuggestionsConversion#registerBlockAttribute()](../../../api/module%5Fcollaboration-core%5Fsuggestions%5Fsuggestionsconversion-SuggestionsConversion.html#function-registerBlockAttribute) methods. For example, if you have a `customCommand` that changes the value of `customAttribute` set on text, use the following snippet: ``` trackChangesEditing.enableDefaultAttributesIntegration( 'customCommand' ); const suggestionsConversion = editor.plugins.get( 'SuggestionsConversion' ); suggestionsConversion.registerInlineAttribute( 'customAttribute' ); ``` Attributes are categorized into two groups: inline (for attributes set on text) and block (for attributes set on model elements). This differentiation is necessary due to the distinct logic for handling suggestions made on different types of content. Each command integrated this way will track all registered attributes and create suggestions for their changes. Each attribute should be registered only once. For attribute formatting each attribute registered in track changes should also have its label registered: ``` const t = editor.t; plugin.descriptionFactory.registerAttributeLabel( 'bold', t( 'bold' ) ); ``` If you require more complex logic when evaluating the suggestion description, refer to [Setting custom suggestion description](#ckeditor5/latest/features/collaboration/track-changes/track-changes-custom-features.html--setting-custom-suggestion-description) section below. #### Tracking attribute changes in custom code In case if some of your custom code makes changes on attributes outside of command execution, you can use the [recordAttributeChanges()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-recordAttributeChanges) method. This method allows you to wrap any code that modifies attributes in a callback, and the track changes plugin will automatically create suggestions for all attribute changes that occur during the callback execution. Here’s an example of using `recordAttributeChanges()` with a custom callback: ``` const trackChangesEditing = editor.plugins.get( 'TrackChangesEditing' ); // Use `recordAttributeChanges` to track changes in your custom code. trackChangesEditing.recordAttributeChanges( () => { editor.model.change( writer => { const currentSelectionRange = editor.model.document.selection.getFirstRange(); // Set bold attribute on the current selection. writer.setAttribute( 'bold', true, currentSelectionRange ); } ); } ); ``` Remember that for the changes to be tracked, the attributes must be registered using either `registerInlineAttribute()` or `registerBlockAttribute()`. ### Other formatting changes The above sections cover typical custom plugins and commands, which simply add a new element or change some attributes. If your feature is more complex, for example, it introduces multiple attributes which values depend on each other, or you command performs multiple related changes at once, then you may need to use a different approach. In such cases, you will need to pass a custom callback to [enableCommand()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-enableCommand) which will overwrite the default command behavior when track changes is on. In the callback you will need to use the track changes API: * [markInlineFormat()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markInlineFormat) – Marks a given range as an inline format suggestion. Used for attribute changes on inline elements and text. * [markBlockFormat()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markBlockFormat) – Marks a format suggestion on a block element. * [markMultiRangeBlockFormat()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markMultiRangeBlockFormat) – Used for format suggestions that contain multiple elements that are not next to one another. Refer to the API documentation of these methods to learn more. Format suggestions are strictly connected with commands. These suggestions data consists of: the range on which command was fired, the command name and the command options. When the suggestion is accepted, the specified command is executed on the suggestion range with the specified options. In other words, the original command execution is “replayed”. The general approach to handle format suggestions for your custom feature is to use a similar logic as in your custom feature but instead of applying changes to the content, create a suggestion using the track changes API: 1. Overwrite the command callback (using `enableCommand()`) so the command is not executed. 2. Use the same logic as in your custom command to decide whether the command should do anything. 3. Use the same logic as in your custom command to evaluate all command parameters that has not been set (and would use a default value if the command’s original code was executed). This is important: a suggestion must have all parameters set, so its execution does not rely on the current content state but on the content state at the time when the suggestion was created. 4. Use the selection range or evaluate a more precise range(s) for the suggestion(s). 5. Use [markInlineFormat()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markInlineFormat) or [markBlockFormat()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-markBlockFormat) to create one or more suggestions using the previously evaluated variables. Note, that if the command is integrated this way, the command change will not be actually executed (in other words, the action will not be reflected in the content). The command will be executed only after the suggestion is eventually accepted. An example of an integration for `imageTypeInline` command: ``` plugin.enableCommand( 'imageTypeInline', ( executeCommand, options ) => { // Commands work on the current selection, so the track // changes integration also works on the current selection. // Find element that will be affected. const image = imageUtils.getClosestSelectedImageElement( editor.model.document.selection ); editor.model.change( () => { plugin.markBlockFormat( image, { // The command to be executed when the suggestion is accepted. commandName: 'imageTypeInline', // Parameters for the command. commandParams: [ options ] }, [], 'convertBlockImageToInline' ); } ); } ); ``` See next section to learn how to generate labels for formatting suggestions. ### Setting custom suggestion description To complete the integration of your command using the format suggestion or for more complex logic for attribute suggestion, you will need to [provide a callback that will generate a description for such a suggestion](../../../api/module%5Ftrack-changes%5Fsuggestiondescriptionfactory-SuggestionDescriptionFactory.html#function-registerDescriptionCallback). This is different from registering a label for insertion/deletion suggestions as format suggestions are more varied and complex. An example of a description callback for the block quote command: ``` plugin.descriptionFactory.registerDescriptionCallback( suggestion => { const { data } = suggestion; if ( !data || data.commandName != 'blockQuote' ) { return; } if ( data.commandParams[ 0 ].forceValue ) { return { type: 'format', content: '*Set format:* block quote' }; } return { type: 'format', content: '*Remove format:* block quote' }; } ); ``` Note that the description callback can be also used for insertion and deletion suggestions if you want to overwrite the default description. source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes-data.html" ## Saving data without suggestions Sometimes you may need to save editor data with all the suggestions accepted or discarded. Common cases include document preview or printing. ### Track changes data plugin To enable saving data without suggestions you will need to use the `TrackChangesData` plugin. It is available in the track changes package. Import it and add to your setup: Then you can use the [track changes data plugin API](../../../api/module%5Ftrack-changes%5Ftrackchangesdata-TrackChangesData.html) to get the editor data with all the suggestions accepted or discarded: ``` const data = await editor.plugins.get( 'TrackChangesData' ).getDataWithAcceptedSuggestions(); ``` Note that this method is asynchronous (it returns a promise that resolves with the editor data). ### Configuring track changes data plugin In most common cases, there is no need for any configuration regarding the track changes data plugin. However, sometimes running the track changes data plugin needs more effort. In general, the track changes data plugin uses a temporary editor instance to load the current data, accept or discard the suggestions and then get the editor data. This may raise some issues in the following scenarios: * if some actions are performed after the editor is initialized (for example, loading some kind of data for your custom plugins), * if you use your own, custom editor class, whose API is different from `ClassicEditor`. In these cases, you can provide your own callback as a configuration parameter for the track changes data plugin: ``` { trackChangesData: { editorCreator: ( config, createElement ) => { // Custom callback. // ... } } } ``` Two parameters are passed to the callback: * `config` \- The editor configuration that should be used to initialize the editor. * `createElement` \- A function that creates a DOM element, should you need one (or more) to initialize your editor. The DOM elements created by this function are hidden and will be cleared after the plugin finishes its work. The callback should return a promise that resolves with the editor instance. An example of a callback using a multi-root editor that requires passing multiple roots in the config parameter of the `.create()` method. ``` { trackChangesData: { editorCreator: ( config, createElement ) => { return CustomMultiRootEditor.create( { ...config, roots: { // ... other config header: { element: createElement(), }, content: { element: createElement(), }, footer: { element: createElement(), } } } ); } } } ``` source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes-granular-suggestions.html" ## Increasing suggestions granularity To maintain a low number of suggestions in the document, in some cases, the track changes feature automatically joins suggestions added by the same user. This happens when, for example, two insertion suggestions are made next to each other, or two formatting suggestions are made on the same text. This leads to a less cluttered UI, and as a result generally provides a better user experience. However, in some specific cases, you may require to have more control over how and when suggestions are joined, and you might want to prevent this automatic mechanism. For this, you can use [config.trackChanges.mergeNestedSuggestions](../../../api/module%5Ftrack-changes%5Ftrackchangesconfig-TrackChangesConfig.html#member-mergeNestedSuggestions) configuration option and [the tracking sessions](#ckeditor5/latest/features/collaboration/track-changes/track-changes-granular-suggestions.html--tracking-sessions) mechanism. ### Auto-merging nested suggestions By default, suggestions **on** an object (such as image or table) will be automatically merged with suggestions **inside** the object (for example, a change in image caption, or a table cell). For example, creating a table and writing some text inside the table will result in one suggestion. This behavior can be changed by setting the [config.trackChanges.mergeNestedSuggestions](../../../api/module%5Ftrack-changes%5Ftrackchangesconfig-TrackChangesConfig.html#member-mergeNestedSuggestions) configuration option to `false`. In the scenario above, there would be two separate suggestions: one for the inserted table and one for the inserted text. ``` { trackChanges: { mergeNestedSuggestions: false } // Other configuration } ``` ### Tracking sessions To stop track changes from automatically joining suggestions made by the same user, [start a new tracking session](#ckeditor5/latest/features/collaboration/track-changes/track-changes-granular-suggestions.html--starting-a-new-tracking-session). Suggestions created in the new tracking session will not be joined with suggestions created in any of the previous tracking session. From the technical point of view, after starting a new tracking session, newly created suggestions will have a unique `trackingSessionId` [attribute](../../../api/module%5Ftrack-changes%5Fsuggestion-Suggestion.html#member-attributes). This will prevent them from being joined to already existing suggestions. If you are using a asynchronous integration, make sure that you correctly save and load suggestion `attributes` together with the rest of the suggestion data. #### Starting a new tracking session To start a new tracking session, call the [TrackChangesEditing#startTrackingSession()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-startTrackingSession) method. From now on, all newly created suggestions will have a unique `trackingSessionId` attribute set. That ID value is returned by the method. ``` editor.plugins.get( 'TrackChangesEditing' ).startTrackingSession(); ``` #### Resuming previous session You can also resume one of the previous tracking sessions by calling [TrackChangesEditing#startTrackingSession()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-startTrackingSession) and passing a previously set `trackingSessionId` as the `id` parameter. ``` editor.plugins.get( 'TrackChangesEditing' ).startTrackingSession( 'somePreviousId' ); ``` To “resume” tracking session for suggestions that were added before introducing [TrackChangesEditing#startTrackingSession()](../../../api/module%5Ftrack-changes%5Ftrackchangesediting-TrackChangesEditing.html#function-startTrackingSession) you may pass `null` as the `id` parameter. It will also stop adding the `trackingSessionId` attribute to new suggestions. ``` editor.plugins.get( 'TrackChangesEditing' ).startTrackingSession( null ); ``` ### Demo 1. Add some text with track changes enabled. You may add more text next to created suggestions to see how the suggestions are automatically expanded. 2. Press the “Start new tracking session” button above the editor, and continue typing after one of the previously created suggestions. 3. See how new suggestion is created instead of expanding the existing suggestion. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Starting a new session on editor initialization One of the primary use cases is to start a new tracking session whenever the user opens the editor and/or after some time has passed. Below you will find an example where a new tracking session is started whenever editor is created. ``` ClassicEditor .create( { // ... Editor configuration ... } ) .then( editor => { // You can store the id if you need it later to resume previous tracking session. const trackingSessionId = editor.plugins.get( 'TrackChangesEditing' ).startTrackingSession(); } ) .catch( /* ... */ ); ``` source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html" ## Integrating track changes with your application The track changes plugin provides an API that lets you manage suggestions added to the document. To save and access suggestions in your database, you first need to integrate this feature. ### Integration methods This guide will discuss two ways to integrate CKEditor 5 with your suggestions data source: * [A simple “load and save” integration](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html--a-simple-load-and-save-integration) using the `TrackChanges` plugin API. * [An adapter integration](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html--adapter-integration) which saves the suggestions data immediately in the database. The adapter integration is the recommended one because it gives you better control over the data. It is also recommended to use the track changes plugin together with the comments plugin. [Check how to integrate the comments plugin](#ckeditor5/latest/features/collaboration/comments/comments-integration.html) with your WYSIWYG editor. ### Before you start #### Preparing a custom editor setup To use the track changes plugin, prepare a custom editor setup with the asynchronous version of the track changes feature included. The easiest way to do that is by using the [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs). Pick a preset and start customizing your editor. **In the “Features” section** of the Builder (2nd step), make sure to: * turn off the “real-time” toggle next to the “Collaboration” group, * enable the “Collaboration → Track Changes” feature. Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step. #### Setting up a sample project Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository: ``` npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project cd sample-project npm install ``` Then, install the necessary dependencies: ``` npm install ckeditor5 npm install ckeditor5-premium-features ``` This project template uses [Vite](https://vitejs.dev/) under the hood and contains 3 source files that we will use: `index.html`, `style.css`, and `main.js`. It is now the time to use our custom editor setup. **Go to the “Installation” section** of the Builder and copy the generated code snippets to those 3 files. #### Activating the feature To use this premium feature, you need to activate it with a license key. Refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. After you have successfully obtained the license key open the `main.js` file and update the `your-license-key` string with your license key. #### Building the project Finally, build the project by running: ``` npm run dev ``` When you open the sample in the browser you should see the WYSIWYG editor with the track changes plugin. However, it still does not load or save any data. You will learn how to add data to the track changes plugin later in this guide. Let’s now dive deeper into the structure of this setup. #### Basic setup’s anatomy Let’s now go through the key fragments of this basic setup. ##### HTML structure The HTML and CSS structure of the page creates two columns: * `
` is the container used by the editor. * `
` is the container used by the sidebar that holds the annotations (namely track changes). ##### JavaScript The `main.js` file sets up the editor instance: * Loads all necessary editor plugins (including the [TrackChanges](../../../api/module%5Ftrack-changes%5Ftrackchanges-TrackChanges.html) plugin). * Sets the `licenseKey` configuration option. * Sets the `sidebar.container` configuration option to the container mentioned above. * Adds the `trackChanges` button to the editor toolbar. #### Comments Track changes use the [comments plugin](#ckeditor5/latest/features/collaboration/comments/comments.html) to allow discussion in suggestions. You should be familiar with the [comments integration](#ckeditor5/latest/features/collaboration/comments/comments-integration.html) guide before you start integrating suggestions. For that reason, the `main.js` file [obtained in the previous step](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html--javascript) does the following on top of the track changes plugin setup: * Loads the [Comments](../../../api/module%5Fcomments%5Fcomments-Comments.html) plugin (dependency of the `TrackChanges` plugin). * Defines the plugin templates: * For the `CommentsIntegration` plugin (learn how to [save comments](#ckeditor5/latest/features/collaboration/comments/comments-integration.html)). * For the `UsersIntegrations` plugin that is shared by both track changes and comments features and we will be used in the next steps of this tutorial. * Adds the `comment` and `commentsArchive` buttons to the editor toolbar. #### Next steps We have set up a simple JavaScript project that runs a basic CKEditor instance with the asynchronous version of the track changes feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods. ### A simple “load and save” integration In this solution, user and suggestions data is loaded during the editor initialization, and suggestions data is saved after you finish working with the editor (for example, when you submit the form containing the WYSIWYG editor). This method is recommended only if you can trust your users or if you provide additional validation of the submitted data to make sure that the user changed their suggestions only. #### Loading the data When the track changes plugin is already included in the editor, you need to create plugins which will initialize users and existing suggestions. First, dump the users and the suggestions data to a variable that will be available for your plugin. ``` // Application data will be available under a global variable `appData`. const appData = { // Users data. users: [ { id: 'user-1', name: 'Mex Haddox' }, { id: 'user-2', name: 'Zee Croce' } ], // The ID of the current user. userId: 'user-1', // Comment threads data. commentThreads: [ { threadId: 'thread-1', comments: [ { commentId: 'comment-1', authorId: 'user-1', content: '

Are we sure we want to use a made-up disorder name?

', createdAt: new Date( '09/20/2018 14:21:53' ), attributes: {} }, { commentId: 'comment-2', authorId: 'user-2', content: '

Why not?

', createdAt: new Date( '09/21/2018 08:17:01' ), attributes: {} } ], context: { type: 'text', value: 'Bilingual Personality Disorder' }, unlinkedAt: null, resolvedAt: null, resolvedBy: null, attributes: {} } ], // Suggestions data. suggestions: [ { id: 'suggestion-1', type: 'insertion', authorId: 'user-2', createdAt: new Date( 2019, 1, 13, 11, 20, 48 ), data: null, attributes: {} }, { id: 'suggestion-2', type: 'deletion', authorId: 'user-1', createdAt: new Date( 2019, 1, 14, 12, 7, 20 ), data: null, attributes: {} }, { id: 'suggestion-3', type: 'attribute:bold|ci1tcnk0lkep', authorId: 'user-1', createdAt: new Date( 2019, 2, 8, 10, 2, 7 ), data: { key: 'bold', oldValue: null, newValue: true }, attributes: { groupId: 'e29adbb2f3963e522da4d2be03bc5345f' } } ], // Editor initial data. initialData: `

Bilingual Personality Disorder

This may be the first time you hear about this made-up disorder but it actually is not that far from the truth. As recent studies show, the language you speak has more effects on you than you realize. According to the studies, the language a person speaks affects their cognition, feelings, behavior, emotions and hence their personality.

This shouldn’t come as a surprise since we already know that different regions of the brain become more active depending on the activity. The structure, information and especially the culture of languages varies substantially and the language a person speaks is an essential element of daily life.

` }; ``` The Builder’s output sample already provides templates of three plugins: `UsersIntegration`, `CommentsIntegration`, and `TrackChangesIntegration`. Replace them with ones that read the data from `appData` and use the [Users](../../../api/module%5Fcollaboration-core%5Fusers-Users.html), [CommentsRepository](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html), and [TrackChanges](../../../api/track-changes.html) APIs, respectively: ``` class UsersIntegration extends Plugin { static get requires() { return [ 'Users' ]; } static get pluginName() { return 'UsersIntegration'; } init() { const usersPlugin = this.editor.plugins.get( 'Users' ); // Load the users data. for ( const user of appData.users ) { usersPlugin.addUser( user ); } // Set the current user. usersPlugin.defineMe( appData.userId ); } } class CommentsIntegration extends Plugin { static get requires() { return [ 'CommentsRepository', 'UsersIntegration' ]; } static get pluginName() { return 'CommentsIntegration'; } init() { const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' ); // Load the comment threads data. for ( const commentThread of appData.commentThreads ) { commentsRepositoryPlugin.addCommentThread( commentThread ); } } } class TrackChangesIntegration extends Plugin { static get requires() { return [ 'TrackChanges', 'UsersIntegration' ]; } static get pluginName() { return 'TrackChangesIntegration'; } init() { const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' ); // Load the suggestions data. for ( const suggestion of appData.suggestions ) { trackChangesPlugin.addSuggestion( suggestion ); } } } ``` Update the `editorConfig.root.initialData` property to use `appData.initialData` value: ``` const editorConfig = { // ... root: { initialData: appData.initialData }, // ... }; ``` And build the project: ``` npm run dev ``` You should now we see an editor instance with one comment thread and several track changes suggestions. #### Saving the data To save the suggestions data you need to get it from the `TrackChanges` API first. To do this, use the [getSuggestions()](../../../api/module%5Ftrack-changes%5Ftrackchanges-TrackChanges.html#function-getSuggestions) method. Then, use the suggestions data to save it in your database in the way you prefer. See the example below. In `index.html` add: ``` ``` In `main.js` update the `ClassicEditor.create()` call with a chained `then()`: ``` ClassicEditor .create( /* ... */ ) .then( editor => { // After the editor is initialized, add an action to be performed after a button is clicked. const trackChanges = editor.plugins.get( 'TrackChanges' ); // Get the data on demand. document.querySelector( '#get-data' ).addEventListener( 'click', () => { const editorData = editor.data.get(); const suggestionsData = trackChanges.getSuggestions( { skipNotAttached: true, toJSON: true } ); // Now, use `editorData` and `suggestionsData` to save the data in your application. // For example, you can set them as values of hidden input fields. console.log( editorData ); console.log( suggestionsData ); } ); } ) .catch( error => console.error( error ) ); ``` #### Demo ``` ``` ### Adapter integration Adapter integration uses an adapter object – provided by you – to immediately save suggestions in your data store. This is the recommended way of integrating track changes with your application because it lets you handle client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side, like the suggestion creation date. You will see how to handle the server response in the following steps. #### Implementation First, define the adapter using the [TrackChanges#adapter](../../../api/module%5Ftrack-changes%5Ftrackchanges-TrackChanges.html#member-adapter) setter. [Adapter methods](../../../api/module%5Ftrack-changes%5Ftrackchanges-TrackChangesAdapter.html) allow you to load and save changes in your database. On the UI side each change in suggestions is performed immediately, however, all adapter actions are asynchronous and are performed in the background. Because of this all adapter methods need to return a `Promise`. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor throws a [CKEditorError](../../../api/module%5Futils%5Fckeditorerror-CKEditorError.html) error, which works nicely together with the [watchdog](#ckeditor5/latest/features/watchdog.html) feature. When you handle the server response you can decide if the promise should be resolved or rejected. While the adapter is saving the suggestion data, a pending action is automatically added to the editor [PendingActions](../../../api/module%5Fcore%5Fpendingactions-PendingActions.html) plugin, so you do not have to worry that the editor will be destroyed before the adapter action has finished. Now you are ready to implement the adapter. If you have set up the sample project as [recommended in the “Before you start” section](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html--before-you-start), open the `main.js` file and add the following code right after the imports: ``` // Application data will be available under a global variable `appData`. const appData = { // Users data. users: [ { id: 'user-1', name: 'Mex Haddox' }, { id: 'user-2', name: 'Zee Croce' } ], // The ID of the current user. userId: 'user-1', // Comment threads data. commentThreads: [ { threadId: 'thread-1', comments: [ { commentId: 'comment-1', authorId: 'user-1', content: '

Are we sure we want to use a made-up disorder name?

', createdAt: new Date( '09/20/2018 14:21:53' ), attributes: {} }, { commentId: 'comment-2', authorId: 'user-2', content: '

Why not?

', createdAt: new Date( '09/21/2018 08:17:01' ), attributes: {} } ], context: { type: 'text', value: 'Bilingual Personality Disorder' }, unlinkedAt: null, resolvedAt: null, resolvedBy: null, attributes: {} } ], // Editor initial data. initialData: `

Bilingual Personality Disorder

This may be the first time you hear about this made-up disorder but it actually is not that far from the truth. As recent studies show, the language you speak has more effects on you than you realize. According to the studies, the language a person speaks affects their cognition, feelings, behavior, emotions and hence their personality.

This shouldn’t come as a surprise since we already know that different regions of the brain become more active depending on the activity. The structure, information and especially the culture of languages varies substantially and the language a person speaks is an essential element of daily life.

` }; ``` The Builder’s output sample already provides templates of three plugins: `UsersIntegration`, `CommentsIntegration`, and `TrackChangesIntegration`. Replace them with ones that read the data from `appData` and use the [Users](../../../api/module%5Fcollaboration-core%5Fusers-Users.html), [CommentsRepository](../../../api/module%5Fcomments%5Fcomments%5Fcommentsrepository-CommentsRepository.html), and [TrackChanges](../../../api/track-changes.html) APIs, respectively: ``` class UsersIntegration extends Plugin { static get requires() { return [ 'Users' ]; } static get pluginName() { return 'UsersIntegration'; } init() { const usersPlugin = this.editor.plugins.get( 'Users' ); // Load the users data. for ( const user of appData.users ) { usersPlugin.addUser( user ); } // Set the current user. usersPlugin.defineMe( appData.userId ); } } class CommentsIntegration extends Plugin { static get requires() { return [ 'CommentsRepository', 'UsersIntegration' ]; } static get pluginName() { return 'CommentsIntegration'; } init() { const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' ); // Load the comment threads data. for ( const commentThread of appData.commentThreads ) { commentsRepositoryPlugin.addCommentThread( commentThread ); } } } class TrackChangesIntegration extends Plugin { static get requires() { return [ 'TrackChanges', 'UsersIntegration' ]; } static get pluginName() { return 'TrackChangesIntegration'; } init() { const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' ); // Set the adapter to the `TrackChanges#adapter` property. trackChangesPlugin.adapter = { getSuggestion: suggestionId => { console.log( 'Getting suggestion', suggestionId ); // Write a request to your database here. // The returned `Promise` should be resolved with the suggestion // data object when the request has finished. switch ( suggestionId ) { case 'suggestion-1': return Promise.resolve( { id: suggestionId, type: 'insertion', authorId: 'user-2', createdAt: new Date(), data: null, attributes: {} } ); case 'suggestion-2': return Promise.resolve( { id: suggestionId, type: 'deletion', authorId: 'user-1', createdAt: new Date(), data: null, attributes: {} } ); case 'suggestion-3': return Promise.resolve( { id: 'suggestion-3', type: 'attribute:bold|ci1tcnk0lkep', authorId: 'user-1', createdAt: new Date( 2019, 2, 8, 10, 2, 7 ), data: { key: 'bold', oldValue: null, newValue: true }, attributes: { groupId: 'e29adbb2f3963e522da4d2be03bc5345f' } } ); } }, addSuggestion: suggestionData => { console.log( 'Suggestion added', suggestionData ); // Write a request to your database here. // The returned `Promise` should be resolved when the request // has finished. When the promise resolves with the suggestion data // object, it will update the editor suggestion using the provided data. return Promise.resolve( { createdAt: new Date() // Should be set on the server side. } ); }, updateSuggestion: ( id, suggestionData ) => { console.log( 'Suggestion updated', id, suggestionData ); // Write a request to your database here. // The returned `Promise` should be resolved when the request // has finished. return Promise.resolve(); } }; // In order to load comments added to suggestions, you // should also integrate the comments adapter. } } ``` Update the `editorConfig.root.initialData` property to use `appData.initialData` value: ``` const editorConfig = { // ... root: { initialData: appData.initialData } // ... }; ``` And build the project: ``` npm run dev ``` You should now we see an editor instance with one comment thread and several track changes suggestions. The adapter is now ready to use with your rich text editor. #### Demo ``` ``` Since the track changes adapter saves suggestions immediately after they are performed, it is also recommended to use the [Autosave](../../../api/module%5Fautosave%5Fautosave-Autosave.html) plugin to save the editor content after each change. #### Why is there no event when I accept or discard a suggestion? Note that when you discard or accept a suggestion, no event is fired in the adapter. This is because the suggestion is never removed from the editor during the editing session. You are able to restore it using undo (Cmd+Z or Ctrl+Z). The same happens when you remove a paragraph with suggestions – there will be no event fired because no data is really removed. However, to make sure that you do not keep outdated suggestions in your database, you should do a cleanup when the editor is destroyed or closed. You can compare the suggestions stored in the editor data with the suggestions stored in your database and remove all the suggestions that are no longer in the editor data from your database. ### Track changes samples Please visit the [ckeditor5-collaboration-samples](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master) GitHub repository to find several sample integrations of the track changes feature. source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes-preview.html" ## Preview final content The content you work on may become heavily edited, including lots of suggestions, possibly coming from different authors. At some point, it may become difficult to understand what the final content will look like, after all suggestions are accepted. When this happens, you may use the track changes preview feature. When used, it will display a modal window with a preview of the content with all the suggestions accepted. ### Demo Use the “Preview final content” button in the track changes toolbar dropdown (you can find it in the menu bar, in “Tools -> Track changes” menu). The editor will display a modal window with the preview. Add more suggestions to the document to see how the final content changes. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Installation To enable the track changes preview feature, add the `TrackChangesPreview` plugin to your editor setup: The plugin will add a new “Preview final content” button to the track changes toolbar dropdown and in the “Tools -> Track changes” menu in the menu bar. You can also execute `previewFinalContent` command to show the modal window with the preview: ``` editor.commands.get( 'previewFinalContent' ).execute(); ``` ### Configuration When the preview is opened, the editor content is put inside a container element, which has the same CSS classes as the editor’s editable element. In most cases this is sufficient to replicate the styling of editor content. If you see that your custom CSS styles are not applied in the preview, make sure that your CSS rules also apply to the HTML in the preview modal. You may need to adjust selectors in your CSS rules to match the preview dialog structure. By default, the editor content is put into a following DOM structure: ```
``` You can target the `ck-track-changes-preview` and `ck-track-changes-preview__root-container` classes, as well as `data-ck-root-name` attribute. However, some integrations may require a custom DOM structure, for example due to complex CSS rules, or if you use [MultiRootEditor](../../../api/module%5Feditor-multi-root%5Fmultirooteditor-MultiRootEditor.html). To provide a custom DOM structure inside the preview modal dialog, use the `trackChanges.preview.renderFunction` configuration option. ``` { trackChanges: { preview: { renderFunction: ( container, elements ) => { // Custom callback inserting the `elements` into `container`. // ... } } } } ``` Two parameters are passed to the callback: * `container` – the main container element that will be inserted into the modal window (this is the element with the `ck-track-changes-preview` CSS class). * `elements` – an array of elements, each holding the contents of one of the editor roots. Unless you are using [MultiRootEditor](../../../api/module%5Feditor-multi-root%5Fmultirooteditor-MultiRootEditor.html), there will be only one element available. They are sorted according to their DOM order. Each element has the `data-ck-root-name` attribute set to the corresponding editor’s root name. You can use them for styling purposes, or to recognize a particular root and handle it in some custom way. Use `renderFunction` to process the `elements` array, wrap them in a DOM structure that will fit your needs, and insert them into `container` element. Below is an example of how `renderFunction` could be used together with a multi-root editor: ``` ClassicEditor .create( { // ... Other configuration options ... trackChanges: { preview: { renderFunction: ( container, elements ) => { for ( const element of elements ) { // Wrap each root into an additional container. const dataContainer = document.createElement( 'div' ); dataContainer.classList.add( 'additional-container' ); dataContainer.appendChild( element ); container.appendChild( dataContainer ); } } } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` This will result in the following structure inside the preview modal window: ```
```
source file: "ckeditor5/latest/features/collaboration/track-changes/track-changes.html" ## Track changes overview This feature enables the track changes mode (also known as the suggestion mode) in CKEditor 5\. This way you can follow the history of changes made by different editors as well as easily accept or decline such changes. ### Demo You can test the track changes feature in the editor below. Use the toolbar dropdown to enable changes tracking and mass accept or decline suggestions. Use the side panel to work with individual changes. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. In this mode, changes done by the users are marked in the content and shown as suggestions in the sidebar. Suggestions can be accepted or discarded by the users. The suggestion balloon is then closed and the change is no longer marked. Apart from reading this guide, we encourage you to read a dedicated blog post which [compares the track changes with revision history](https://ckeditor.com/blog/ckeditor-5-comparing-revision-history-with-track-changes/) and another blog post discussing [CKEditor 5’s collaboration features and their real-life implementations](https://ckeditor.com/blog/Feature-of-the-month-Collaborative-writing-in-CKEditor-5/). ### Integration #### Use as a standalone plugin The track changes feature does not require real-time collaboration to work. If you prefer a more traditional approach to document editing, track changes can be added to CKEditor 5 just like any other plugin. To learn how to integrate track changes as a standalone plugin, please refer to the [Integrating track changes with your application](#ckeditor5/latest/features/collaboration/track-changes/track-changes-integration.html) guide. #### Use with real-time collaboration If you are using the real-time collaboration feature, refer to the [Real-time collaboration features integration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) guide. ### Learn more After you run the track changes feature in your editor, you may want to learn more about it. Find out below how it works, how it can be configured, customized, and extended, to fit your application the best way possible. #### Configuration The configuration for the track changes feature can be found in the [TrackChangesConfig](../../../api/module%5Ftrack-changes%5Ftrackchangesconfig-TrackChangesConfig.html) API reference. #### Suggestions markup Suggestions are always attached to some place in the document. To make sure that they will not be lost, the track changes plugin adds some special markup to the document: * `` and `` tags are added if the suggestion starts/ends in text, * otherwise, the following attributes are added to elements: * `data-suggestion-start-before`, * `data-suggestion-end-after`, * `data-suggestion-start-after`, * `data-suggestion-end-before`. Also, the `` tag is used for table cells pasting suggestions to separate old content (original) from the new content (pasted). [Read more about marker-to-data conversion](../../../api/module%5Fengine%5Fconversion%5Fdowncasthelpers-DowncastHelpers.html#function-markerToData) to understand what data you may expect. Examples of the possible markup: Replacing the word “chocolate” with the word “ice-cream”: ```

I like ice-cream chocolate.

``` Inserting image: ```
Image caption.
``` Adding bold: ```

This is important .

``` Pasting a table cell with the text “New” into a table cell with the text “Old”: ```

New

Old

``` Note that if your application filters HTML content, for example, to prevent XSS, make sure to leave the suggestion tags and attributes in place when saving the content in the database. The suggestion markup is necessary for further editing sessions.
#### Suggestions attributes Suggestion attributes is additional data stored with the suggestions and used by other features. You can also use them to set and read custom data necessary by your custom features built around suggestions. ``` suggestion.setAttribute( 'isImportant', true ); ``` You can group multiple values in an object, using dot notation: ``` suggestion.setAttribute( 'customData.type', 'image' ); suggestion.setAttribute( 'customData.src', 'foo.jpg' ); ``` Attributes set on the suggestion can be accessed through `attribute` property: ``` const isImportant = suggestion.attributes.isImportant; const type = suggestion.attributes.customData.type; ``` You can also observe `attributes` property or bind other properties to it: ``` myObj.bind( 'customData' ).to( suggestion, 'attributes', attributes => attributes.customData ); ``` Whenever [setAttribute()](../../../api/module%5Ftrack-changes%5Fsuggestion-Suggestion.html#function-setAttribute) or [removeAttribute()](../../../api/module%5Ftrack-changes%5Fsuggestion-Suggestion.html#function-removeAttribute) is called, the `attributes` property is re-set, and observables are refreshed. Using these fires the `update` method in an adapter. #### Saving the data without suggestions If you need to get the editor data with all the existing suggestions accepted or discarded, please refer to the [dedicated guide](#ckeditor5/latest/features/collaboration/track-changes/track-changes-data.html). #### Saving the data with suggestion highlights By default, the data returned by `editor.getData()` contains the markup for the suggestions as described above. It does not provide the markup that visually shows the suggestion highlights in the data (similarly to how they are shown in the editor). It is possible to change the editor output using the `showSuggestionHighlights` option passed in `editor.getData()`. When set, the editor output will return suggestions similarly to how they are present inside the editor: ``` editor.getData( { showSuggestionHighlights: true } ); ``` Will return: ```

Foo bar

``` The [export to PDF feature](#ckeditor5/latest/features/converters/export-pdf.html) can be integrated with the suggestion highlights as shown below: ``` { exportPdf: { // More configuration of the Export to PDF. // ... dataCallback: editor => editor.getData( { showSuggestionHighlights: true } ) } } ```
#### Force track changes mode to be enabled If you would like to have track changes enabled by default, execute the `trackChanges` command, which toggles the track changes mode. It should be done after the editor instance is initialized: ``` ClassicEditor .create( { // Editor's configuartion. // ... } ) .then( editor => { editor.execute( 'trackChanges' ); } ) .catch( error => console.error( error ) ); ``` You can disable the `trackChanges` command to prevent turning the track changes on or off. This can be useful for example when a particular user has permissions only to create suggestions in a given document. You can disable the command by calling [Command#forceDisabled()](../../../api/module%5Fcore%5Fcommand-Command.html#function-forceDisabled): ``` ClassicEditor .create( { // Editor's configuartion. // ... } ) .then( editor => { editor.execute( 'trackChanges' ); editor.commands.get( 'trackChanges' ).forceDisabled( 'suggestionsMode' ); } ) .catch( error => console.error( error ) ); ``` To prevent a user from accepting or discarding suggestions, disable commands responsible for these actions: ``` ClassicEditor .create( { // Editor's configuartion. // ... } ) .then( editor => { editor.execute( 'trackChanges' ); editor.commands.get( 'trackChanges' ).forceDisabled( 'suggestionsMode' ); editor.commands.get( 'acceptSuggestion' ).forceDisabled( 'suggestionsMode' ); editor.commands.get( 'acceptAllSuggestions' ).forceDisabled( 'suggestionsMode' ); editor.commands.get( 'discardAllSuggestions' ).forceDisabled( 'suggestionsMode' ); editor.commands.get( 'discardSuggestion' ).forceDisabled( 'suggestionsMode' ); } ) .catch( error => console.error( error ) ); ``` Keep in mind that the `'suggestionsMode'` identifier can be later used to enable commands using [Command#clearForceDisabled()](../../../api/module%5Fcore%5Fcommand-Command.html#function-clearForceDisabled). #### Markers styling Similarly to everywhere in the CKEditor 5 Ecosystem, we have used [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fvariables) to let the developers customize the design of such UI elements as, for example, suggestion markers. You can override these properties with a `.css` file or place your customizations directly into the `` section of your page, but in this case, you will need to use a more specific CSS selector than `:root` (like ``). Here you can find the default CSS Variables used for the track changes feature: ``` :root { /* You can override the design of suggestion markers in the content. */ /* Variables responsible for suggestions for text: */ --ck-color-suggestion-marker-insertion-border: hsla(128, 71%, 40%, .35); --ck-color-suggestion-marker-insertion-border-active: hsla(128, 71%, 25%, .5); --ck-color-suggestion-marker-insertion-background: hsla(128, 71%, 65%, .35); --ck-color-suggestion-marker-insertion-background-active: hsla(128, 71%, 50%, .5); --ck-color-suggestion-marker-deletion-border: hsla(345, 71%, 40%, .35); --ck-color-suggestion-marker-deletion-border-active: hsla(345, 71%, 25%, .5); --ck-color-suggestion-marker-deletion-background: hsla(345, 71%, 65%, .35); --ck-color-suggestion-marker-deletion-background-active: hsla(345, 71%, 50%, .5); --ck-color-suggestion-marker-deletion-stroke: hsla(345, 71%, 20%, .5); --ck-color-suggestion-marker-format-border: hsla(191, 90%, 40%, .4); --ck-color-suggestion-marker-format-border-active: hsla(191, 90%, 40%, .65); /* Variables responsible for the left border of the suggestion boxes in the sidebar: */ --ck-color-comment-box-border: hsl(55, 98%, 48%); --ck-color-suggestion-box-deletion-border: hsl(345, 62%, 60%); --ck-color-suggestion-box-insertion-border: hsl(128, 62%, 60%); --ck-color-suggestion-box-format-border: hsl(191, 62%, 60%); /* Variables responsible for the styling of suggestions for widgets: */ --ck-color-suggestion-widget-insertion-background: hsla(128, 71%, 65%, .05); --ck-color-suggestion-widget-insertion-background-active: hsla(128, 71%, 50%, .07); --ck-color-suggestion-widget-deletion-background: hsla(345, 71%, 65%, .05); --ck-color-suggestion-widget-deletion-background-active: hsla(345, 71%, 45%, .07); --ck-color-suggestion-widget-format-background: hsla(191, 90%, 40%, .09); --ck-color-suggestion-widget-format-background-active: hsla(191, 90%, 40%, .16); --ck-color-suggestion-widget-th-insertion-background: hsla(128, 71%, 65%, .1); --ck-color-suggestion-widget-th-insertion-background-active: hsla(128, 71%, 50%, .12); --ck-color-suggestion-widget-th-deletion-background: hsla(345, 71%, 65%, .1); --ck-color-suggestion-widget-th-deletion-background-active: hsla(345, 71%, 45%, .12); } ``` #### API overview Check the [track changes API documentation](../../../api/track-changes.html) for detailed information about the track changes API. Making yourself familiar with the API may help you understand the code snippets. #### Suggestions annotations customization The suggestions annotations are highly customizable. Please refer to the [Annotation customization](#ckeditor5/latest/features/collaboration/annotations/annotations.html) guide to learn more. #### Integration with custom features If you provide your own plugins, you may want to [integrate these custom features with track changes mode](#ckeditor5/latest/features/collaboration/track-changes/track-changes-custom-features.html). source file: "ckeditor5/latest/features/collaboration/users.html" ## Users The [Users](../../api/module%5Fcollaboration-core%5Fusers-Users.html) plugin and related plugins let you manage user data and permissions. This is essential when many users are working on the same document. ### Additional feature information The users plugin is automatically provided if you load any collaboration features plugins. You must set the users data before you load the collaboration features. To do that, create a plugin that requires all collaboration features you use and defines the users. ``` // An example of a plugin that provides user data for an editor // that uses the `Comments` and `RevisionHistory` plugins. class UsersIntegration extends Plugin { static get requires() { return [ 'Comments', 'RevisionHistory' ]; } init() { const users = this.editor.plugins.get( 'Users' ); // Provide user data from your database. users.addUser( { id: 'u1', name: 'Zee Croce' } ); users.addUser( { id: 'u2', name: 'Mex Haddox' } ); // Define the local user. users.defineMe( 'u1' ); } } // More code. // ... ClassicEditor .create( { extraPlugins: [ UsersIntegration ], // More editor's configuration. // ... } ); ``` ### Local user (“me” user) A local user (also called “me” user) is regarded as the one who uses the editor instance. Features (like comments or revisions history) will attribute changes to that user. You can access the “me” user via `Users` plugin: ``` const usersPlugin = editor.plugins.get( 'Users' ); // We get either `User` or `undefined` if the local user is not set. const localUser = usersPlugin.me; ``` You can also use the `isMe` flag on the `User` item to check if it is the local user: ``` const id = 'e1d1a156789abc92e4b9affff124455bb'; const user = editor.plugins.get( 'Users' ).getUser( id ); // It is `true` if it is the "me" user, `false` otherwise. const isOwnUser = user.isMe; ``` The local user’s avatar is highlighted in all places it is displayed. You can easily override this behavior using the `.ck-user_me` CSS class selector. ``` /* Display the local user the same as other users. */ .ck-user.ck-user_me { border: none; outline: none; } ``` ### Anonymous user If for some reason you do not want to, or you cannot, provide the user data, you can use the anonymous user: ``` const usersPlugin = editor.plugins.get( 'Users' ); usersPlugin.useAnonymousUser(); usersPlugin.me.name; // 'Anonymous' usersPlugin.me.id; // 'anonymous-user' ``` The anonymous user’s avatar is a contour of a human face. You can set the anonymous user’s ID using the `config.users.anonymousUserId` property: ``` ClassicEditor .create( { // More editor's configuration. // ... users: { anonymousUserId: '0' } } ); ``` ### User permissions In many applications, the document creation workflow consists of several precisely defined steps such as content creation, discussion, proofreading, final review and acceptance, etc. The users of such an application may have certain roles and permissions. You can change the permissions for a given user, which results in enabling or disabling some editor functionalities. You can set the permissions using the [Permissions](../../api/module%5Fcollaboration-core%5Fpermissions-Permissions.html) plugin. It is automatically provided if you load any of the collaboration features plugins. It is a good practice to set permissions directly after defining the users: ``` class UsersIntegration extends Plugin { // More methods. // ... init() { const users = this.editor.plugins.get( 'Users' ); // Provide user data from your database. users.addUser( { id: 'u1', name: 'Zee Croce' } ); users.addUser( { id: 'u2', name: 'Mex Haddox' } ); // Define the local user. users.defineMe( 'u1' ); // Set permissions. const permissions = this.editor.plugins.get( 'Permissions' ); // "Commentator" role. permissions.setPermissions( [ 'comment:write' ] ); } } ``` The full list of defined permissions is available in the [Permissions](../../api/module%5Fcollaboration-core%5Fpermissions-Permissions.html) plugin description. ### Operation authors The [Users#getOperationAuthor()](../../api/module%5Fcollaboration-core%5Fusers-Users.html#function-getOperationAuthor) method gives you the ability to check which user created a given operation. This is useful when creating custom features in integrations using [real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html). There are two cases when the operation author might be `null`: 1. For initial operations (fetched from the server when connecting). 2. For some automatically created operations that are not meaningful (`NoOperation`s). Below is an example of using `getOperationAuthor()` to find out which user was the last to edit the document. In this case, you should skip `NoOperation`s and some `MarkerOperation`s since they do not affect the document content. ``` let lastUser = null; editor.model.on( 'applyOperation', ( evt, args ) => { const users = editor.plugins.get( 'Users' ); const operation = args[ 0 ]; if ( operation.isDocumentOperation && affectsData( operation ) ) { const user = users.getOperationAuthor( operation ); if ( user && user != lastUser ) { lastUser = user; console.log( lastUser.name, lastUser, operation ); } } function affectsData( operation ) { return operation.type != 'noop' && ( operation.type != 'marker' || operation.affectsData ); } } ); ``` ### Customize initials To customize how [user initials](../../api/module%5Fcollaboration-core%5Fusers-User.html#member-initials) are generated, set the [getInitialsCallback](../../api/module%5Fcollaboration-core%5Fconfig-CollaborationUsersConfig.html#member-getInitialsCallback) configuration option when initializing the editor: ``` ClassicEditor .create( { // More editor's configuration. // ... users: { getInitialsCallback: ( name: string ) => { // Custom logic to generate initials. return name.split( ' ' )[ 0 ].charAt( 0 ) + name.split( ' ' )[ 1 ].charAt( 0 ); } } } ); ``` ### Theme customization #### User avatar You can define the user’s avatar appearance by modifying these CSS variables: ``` :root { --ck-user-avatar-size: 40px; --ck-user-avatar-background: hsl(210, 52%, 44%); --ck-user-name-color: hsl(0, 0%, 100%); /* Border color used to highlight the local user. */ --ck-user-me-border-color: hsl(0, 0%, 100%); } ``` #### User colors You can also define colors used to represent the selection of other users. User colors are defined using [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using%5FCSS%5Fvariables). There are 8 user colors defined by default. You can add more colors or change the default colors. Color variables with an alpha channel (`--ck-user-colors--$(number)-alpha`) are used for selection highlights. The solid color variables (`--ck-user-colors--$(number)`) are used in the rest of the user UI elements. You are welcome to change this color palette to fit your UI. ``` /* The current color set for users in the collaboration plugins. */ :root { --ck-user-colors--0: hsla(235, 73%, 67%, 1); --ck-user-colors--0-alpha: hsla(235, 73%, 67%, 0.15); --ck-user-colors--1: hsla(173, 100%, 24%, 1); --ck-user-colors--1-alpha: hsla(173, 100%, 24%, 0.15); --ck-user-colors--2: hsla(0, 46%, 50%, 1); --ck-user-colors--2-alpha: hsla(0, 46%, 50%, 0.15); --ck-user-colors--3: hsla(256, 54%, 45%, 1); --ck-user-colors--3-alpha: hsla(256, 54%, 45%, 0.15); --ck-user-colors--4: hsla(95, 50%, 36%, 1); --ck-user-colors--4-alpha: hsla(95, 50%, 36%, 0.15); --ck-user-colors--5: hsla(336, 78%, 43%, 1); --ck-user-colors--5-alpha: hsla(336, 78%, 43%, 0.15); --ck-user-colors--6: hsla(0, 80%, 59%, 1); --ck-user-colors--6-alpha: hsla(0, 80%, 59%, 0.15); --ck-user-colors--7: hsla(184, 90%, 43%, 1); --ck-user-colors--7-alpha: hsla(184, 90%, 43%, 0.15); } ``` These colors are, among others, used in the [users presence list](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html--users-presence-list) to represent users. #### Adding more user colors You can define additional colors for users if you find the default set too small. First, prepare a CSS file with some color definitions: ``` /* mycolors.css */ :root { --ck-user-colors--8: hsla(31, 90%, 43%, 1); --ck-user-colors--8-alpha: hsla(31, 90%, 43%, 0.15); --ck-user-colors--9: hsla(61, 90%, 43%, 1); --ck-user-colors--9-alpha: hsla(61, 90%, 43%, 0.15); } ``` Then, import this CSS file and specify the `colorsCount` configuration option: ``` import './mycolors.css'; ClassicEditor .create( { // More editor's configuration. // ... users: { colorsCount: 10 } } ); ``` source file: "ckeditor5/latest/features/converters/export-pdf.html" ## Export to PDF The export to PDF feature lets you generate a PDF file directly from the editor. ### Demo The demo below lets you generate a PDF file based on the editor’s content. Edit the document, then click the export to PDF toolbar button to save the content as a PDF. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### How it works The PDF export feature collects the HTML [generated with the editor.getData() method](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-dataCallback) and the [default editor content styles](#ckeditor5/latest/getting-started/setup/css.html) combined with the styles provided by you in the configuration. It then sends them to the CKEditor Cloud Services HTML to PDF converter service. The service generates a PDF file and returns it to the user’s browser so they can save it in the PDF format on their disk. The crucial aspect of this feature is its [configuration](#ckeditor5/latest/features/converters/export-pdf.html--configuration). To ensure that the generated PDF looks as close as possible to the same content displayed in the WYSIWYG editor, the feature must be carefully configured. The complementary [pagination feature](#ckeditor5/latest/features/pagination/pagination.html) allows you to see where page breaks would be after you export the document to PDF. Thanks to the live preview, the user can fine-tune the structure of the output document when editing it. The pagination feature also shows you the page count and lets you navigate between the document pages. ### Integration with merge fields (content placeholders) Merge fields are visually distinct placeholder elements you can put into the content to mark places where real values should be inserted. It is perfect for creating document templates and other kinds of personalized content. This allows for automation and creating batch output of personalized PDF files. Learn how to configure it in the [proper section of the merge fields guide](#ckeditor5/latest/features/merge-fields.html--using-callbacks-to-define-values). ### Before you start After you select a plan, follow the steps below, as explained in the [Export to PDF quick start guide](#cs/latest/guides/export-to-pdf/quick-start.html): * [Log into the CKEditor Ecosystem customer dashboard](#cs/latest/guides/export-to-pdf/quick-start.html--log-in-to-the-customer-portal). * [Create the token endpoint needed for authorization](#cs/latest/guides/export-to-pdf/quick-start.html--creating-token-endpoint). * [Install](#ckeditor5/latest/features/converters/export-pdf.html--installation) and [configure](#ckeditor5/latest/features/converters/export-pdf.html--configuration) the CKEditor 5 export to PDF plugin. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuration The configuration is the key to allow the HTML to PDF converter service to generate PDF documents that look as close as possible to the content created in the rich-text editor. The configuration consists of 3 main parts: * The [converter options](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-converterOptions) that tell the HTML to PDF converter service what is the format of the page (A4, Letter), the page orientation, etc. * The [style sheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets) sent to the service. They allow styling the content in the PDF document with the same styles that are applied to the content in the editor. * The [content styles used in the editor](#ckeditor5/latest/getting-started/setup/css.html) when rendered on the page. These options need to stay in sync. For example: * The style sheets sent to the service must define the same typography that is used in the editor. * The editor’s content container should be styled in a way that reflects the page size and margins defined in the converter options. * All web fonts defined on the page where the editor is used must be sent to the service as well. Read on to learn how to achieve this. #### Default configuration This is the default configuration of the PDF export feature for CKEditor 5. ``` { exportPdf: { fileName: 'document.pdf', converterUrl: 'https://pdf-converter.cke-cs.com/v2/convert/html-pdf', stylesheets: [ './ckeditor5-content.css' ], converterOptions: { document: { size: 'A4', orientation: 'portrait', margins: { top: '0mm', bottom: '0mm', right: '0mm', left: '0mm' } }, rendering: { wait_for_network: true, wait_time: 0 } }, dataCallback: ( editor ) => editor.getData() } } ``` If you are using the EU cloud region, remember to adjust the endpoint: ``` exportPdf: { converterUrl: 'https://pdf-converter.cke-cs-eu.com/v2/convert/html-pdf' } ``` #### `stylesheets` option Use the `stylesheets` option to provide paths (relative or absolute URLs) to all style sheets that should be included during the HTML to PDF conversion. The rule of thumb is if you want the export to preserve the styles, always add the style sheets with the content styles of the editor. Their path depends on your application setup, for example: ``` { exportPdf: { stylesheets: [ './ckeditor5-content.css' './styles.css' ], // ... } } ``` In the snippet above, we assume both style sheets are available via the relative path on the client side. For example, some frameworks allow to place files in the `public` folder. #### Plugin options For some use cases the default configuration will suffice. As you can see in the example above, you can improve how your PDF file will look by adjusting the PDF export plugin configuration. * **[config.exportPdf.stylesheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets)** You can set the paths (relative or absolute URLs) to the style sheets that should be included during the HTML to PDF conversion. * **[config.exportPdf.fileName](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-fileName)** Sets the name for the generated PDF file (together with the extension). The default name is `document.pdf`. You can see it called in the [default configuration](#ckeditor5/latest/features/converters/export-pdf.html--default-configuration) listing above. This option, however, also allows for using a callback to generate a dynamic file name. In the example below, the document’s title will be used as the file name of the generated PDF file. ``` // Dynamic file name. const exportPdfConfig = { fileName: () => { const articleTitle = document.querySelector( '#title' ); return `${ articleTitle.value }.pdf`; } } ``` * **[config.exportPdf.converterUrl](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-converterUrl)** By default, the PDF export feature is configured to use the CKEditor Cloud Services HTML to PDF converter service to generate the PDF files. You can, however, use this option to provide the URL to an on-premises converter. [Contact us](https://ckeditor.com/contact/) if you need this feature. If you are using the EU cloud region, adjust the endpoint accordingly: ``` exportPdf: { converterUrl: 'https://pdf-converter.cke-cs-eu.com/v2/convert/html-pdf' } ``` * **[config.exportPdf.tokenUrl](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-tokenUrl)** A token URL or a token request function. This field is optional and you should use it when you require a different [tokenUrl](../../api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-tokenUrl) for the export to PDF feature. You can skip this option if you use the `cloudServices` configuration to provide the same `tokenUrl`. In most cases you will probably want to provide the token in `cloudServices`, as other plugins like [real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) will use this token as well. In this guide, to explicitly show that this value is needed, we leave it the inside `exportPdf` configuration. * **[config.exportPdf.converterOptions](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-converterOptions)** The converter options control the PDF output: page size, margins, headers, footers, metadata, security, and more. See the [HTML to PDF Converter features](#ckeditor5/latest/features/converters/export-pdf.html--html-to-pdf-converter-features) section for details on each capability, or refer to the [API documentation](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConverterOptionsV2.html) for the full reference. Below is a sample configuration: ``` converterOptions: { document: { size: 'A4', orientation: 'portrait', margins: { top: '20mm', bottom: '20mm', right: '12mm', left: '12mm' } } } ``` * **[config.exportPdf.dataCallback](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-dataCallback)** By default, the plugin uses `editor.getData()` to gather the HTML sent to the conversion service. You can use this option to customize the editor’s data. For example, use this setting to enable [highlighting tracked changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) and [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) in the exported PDF file. ``` dataCallback: editor => editor.getData( { showSuggestionHighlights: true, showCommentHighlights: true } ), ``` #### Export to PDF V1 (deprecated) The V1 API uses a flat configuration structure for `converterOptions`, unlike the nested structure in V2: ``` { exportPdf: { version: 1, // Required to use V1 converterOptions: { format: 'A4', margin_top: '0mm', margin_bottom: '0mm', margin_right: '0mm', margin_left: '0mm', page_orientation: 'portrait', header_html: '
Header content
', footer_html: '
', header_and_footer_css: '#header, #footer { background: hsl(0, 0%, 95%); } .styled { font-weight: bold; } .styled-counter { font-size: 1em; }', wait_for_network: true, wait_time: 0 } } } ``` For the complete list of V1 options, see the [V1 API documentation](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConverterOptions.html) and the [V1 REST API documentation](https://pdf-converter.cke-cs.com/v1/convert/docs).
### HTML to PDF converter features #### Page setup Configure the page size, orientation, and margins through the `document` object in `converterOptions`: ``` converterOptions: { document: { size: 'A4', orientation: 'portrait', margins: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' } } } ``` The `size` property accepts predefined formats (`'A4'`, `'Letter'`, `'Legal'`, `'A3'`, etc.) or a custom object with `width` and `height`. For a complete example including matching editor CSS, see [Setting the page format](#ckeditor5/latest/features/converters/export-pdf.html--setting-the-page-format). ``` document: { size: { width: '210mm', height: '297mm' } } ``` #### Setting the base URL To enable proper resolution of relative URLs for images and links, pass the `base_url` option: ``` converterOptions: { base_url: 'https://example.com' } ``` For editor content like: ```

Homepage

Logo

``` this option will resolve the URLs into their absolute forms: * `/` will become `https://example.com`, * `/logo.png` will become `https://example.com/logo.png`. #### Images Currently, the converter only supports absolute URLs and `Base64`\-encoded images. See the [REST API documentation](https://pdf-converter.cke-cs.com/v2/convert/docs) for details. #### Web fonts If you are using web fonts via an `@import` or `@font-face` declaration, you can pass the path(s) to the `.css` file(s) containing them to the [config.exportPdf.stylesheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets). The order of the provided paths matters – you should list style sheets with web font declarations first. For more technical details, check the [API documentation](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html) and [REST API documentation](https://pdf-converter.cke-cs.com/v2/convert/docs). #### Rendering options Fine-tune how the converter renders the page before generating the PDF: ``` converterOptions: { rendering: { wait_for_network: true, wait_time: 0, wait_for_selector: '.content-loaded' } } ``` * `wait_for_network` – When `true`, the converter waits for all network requests to finish before rendering. * `wait_time` – Additional wait time in milliseconds (0–15000) after the page loads. * `wait_for_selector` – A CSS selector; the converter waits until an element matching this selector appears in the DOM. #### Headers and footers The converter lets you set the document’s header and footer similarly to Microsoft Word or Google Docs. Define a `default` header and footer applied to all pages: ``` converterOptions: { document: { margins: { top: '15mm', bottom: '15mm', right: '15mm', left: '15mm' } }, headers: { default: { html: '
Header content
', css: '#header { background: hsl(0, 0%, 95%); } .styled { font-weight: bold; text-align: center; }' } }, footers: { default: { html: '
', css: '#footer { background: hsl(0, 0%, 95%); } .styled-counter { font-size: 1em; color: hsl(0, 0%, 60%); }' } } } ``` You can set a background for the header or footer using the `#header` or `#footer` selector in the `css` option. These wrapper elements are provided by the converter.
##### Per-page-type headers and footers You can define different headers and footers for the first page, odd pages, and even pages using the `first`, `odd`, and `even` keys. Each overrides the `default` for its respective pages: ``` headers: { default: { html: '
Company Name
', css: '.header { text-align: center; font-size: 10pt; }' }, first: { html: '
Report Title
', css: '.header-first { text-align: center; font-size: 14pt; font-weight: bold; }' }, odd: { html: '
Section
', css: '.header-odd { text-align: right; }' }, even: { html: '
Section
', css: '.header-even { text-align: left; }' } } ``` The `footers` object follows the same structure. If a page type key is not defined, the `default` is used as a fallback. As you can see in the examples above, you can use the `pageNumber` and `totalPages` placeholders. For more details, refer to the [REST API documentation](https://pdf-converter.cke-cs.com/v2/convert/docs). If you import headers and footers from a Word document and want to preserve and reapply them when exporting, see the Import from Word guide’s [Preserving headers and footers](#ckeditor5/latest/features/converters/import-word/import-word.html--preserving-headers-and-footers). Note that when forwarding stored headers/footers into PDF export, you typically need to inject those header/footer converter options within the `execute` call of the `exportPdf` command. Hence, the converter receives them at export time.
#### Mirror margins Mirror margins (also known as “gutter” or “book” margins) are useful for documents intended for double-sided printing or binding. When `enable_mirror_margins` is set to `true`, the left and right margins swap between odd and even pages: * **Odd pages (right-hand)**: `left` becomes the inner margin (near binding), `right` becomes the outer margin. * **Even pages (left-hand)**: the margins are reversed – `right` becomes the inner margin, `left` becomes the outer margin. ``` converterOptions: { document: { margins: { top: '20mm', bottom: '20mm', left: '25mm', right: '15mm', enable_mirror_margins: true } } } ``` In this example, odd pages will have a 25 mm inner margin and 15 mm outer margin, while even pages will swap to 15 mm inner and 25 mm outer, creating a mirrored layout suitable for book binding. #### Document metadata Set PDF properties such as title, author, subject, keywords, and custom fields. PDF readers display this information in their document properties panel. ``` converterOptions: { metadata: { title: 'Quarterly Report', author: 'Jane Smith', subject: 'Q1 2026 Financial Summary', keywords: [ 'finance', 'quarterly', 'report' ], custom_fields: { 'Department': 'Marketing', 'Document-ID': 'DOC-12345' } } } ``` #### Security and encryption Protect generated PDFs with an owner password. The owner password controls document permissions (printing, copying, modifying). The PDF is encrypted with AES-256 by default. ``` converterOptions: { security: { owner_password: 'securePassword123' } } ``` The `owner_password` is required when using this feature and must be between 6 and 64 characters. #### Digital signatures Sign documents with PKCS#12 certificates to verify authenticity. The signature is embedded invisibly in the PDF metadata – it does not add visual elements to the document pages. PDF readers will show signature details in their signature panel. ``` converterOptions: { signature: { certificate: 'base64EncodedPKCS12Certificate', certificate_password: 'certificatePassword', reason: 'Document approval', location: 'New York, USA' } } ``` Both `certificate` (base64-encoded `.p12`/`.pfx` file) and `certificate_password` are required. The `reason` and `location` fields are optional metadata. #### Compression control By default, the converter compresses PDF output for smaller file sizes. You can disable compression to preserve the original PDF structure: ``` converterOptions: { disable_compression: true } ``` #### Other * By default, the generated PDF file is encoded with **`UTF-8`**. * By default, the converter sets **`color-adjust: exact;`**. This means that your PDF document will preserve colors, images, and styles as you can see them in the editor. * The generated document can be watermarked. [See the example and demo in the section below.](#ckeditor5/latest/features/converters/export-pdf.html--adding-a-watermark-to-the-document) ### Examples Check out some configuration examples that will show you how to customize the export to PDF feature. In the first example, you will learn how to add custom styling. The second example will show you how to set the page format. In the third one, you can see how to use web fonts in your configuration. #### Re-using custom editor styling The default configuration of the [ExportPdf](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdf.html) plugin attaches the default editor content styles to the HTML content sent to the converter. However, if you need to, you can also set paths to additional CSS files. Let us assume that you already have a `my-custom-editor-styles.css` with your custom styling for the editor content that you use on your website, but you also want to include these styles in the generated PDF file. Here is the example code: ``` ClassicEditor .create( { // ... Other configuration options ... exportPdf: { stylesheets: [ 'path/to/editor-styles.css', 'path/to/my-styles.css' ], fileName: 'my-document.pdf' } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` This is how the corresponding editor styles may look like: ``` /* my-custom-editor-styles.css */ /* Custom link color. */ .ck.ck-content a { color: purple; } /* Custom header styling. */ .ck.ck-content h1 { border-bottom: 2px solid; } /* Another custom styling... */ /* ... */ ``` With these settings, the content in the generated PDF file should have the same styling as it has in the WYSIWYG editor. #### Setting the page format Consistency is an important factor. To make sure that the editor content and the generated PDF file look the same, you need to match their format settings. You can change your existing style sheet or use a new one, for example, `format.css`. By default, the CKEditor Cloud Services HTML to PDF converter is set to A4 format, but you may change this setting in your configuration. Assuming that you want to create a document in the US Letter format, with the standard margins (`19mm` for each side), here is the example code you can use: ``` ClassicEditor .create( { // ... Other configuration options ... exportPdf: { stylesheets: [ 'path/to/editor-styles.css', 'path/to/my-styles.css' ], fileName: 'my-document.pdf', converterOptions: { document: { // Document format settings with proper margins. size: 'Letter', orientation: 'portrait', margins: { top: '19mm', bottom: '19mm', right: '19mm', left: '19mm' } } } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Now set the corresponding editor styles: ``` /* format.css */ /* Styles for the editable. */ .ck.ck-content.ck-editor__editable { /* US Letter size. */ width: 215.9mm; /* Padding is your document's margin. */ padding: 19mm; /* You do not want to change the size of the editor by applying the new padding values. */ box-sizing: border-box; /* ... */ } ``` With these settings, the content in the generated PDF file should have the same US Letter format layout as it has in the editor. #### Providing web font styles This time you want to add a web font to your plugin. Let us assume that the editor is used on a page with certain typography, so the content in the editor inherits the font styles from the page. As such, these styles need to be passed to the HTML to PDF converter service. The example below uses a web font from the Google Fonts service. For your convenience, the `@import` declaration and any font styles needed for your website are kept in a separate file, for example, `fonts.css`. ``` /* fonts.css */ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700&display=swap'); html, body { font-family: "Source Sans Pro", sans-serif; /* ... */ } /* ... */ ``` This allows you to use the web font settings in the plugin, without any additional tweaks. Just pass the path to the file in the [config.exportPdf.stylesheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets) configuration option. The order matters here. Font declarations should be at the beginning of the style sheets’ array. Otherwise, there is no guarantee that the font styling will be applied to the PDF file. Refer to the [config.exportPdf.stylesheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets) API documentation for more details. ``` ClassicEditor .create( { // ... Other configuration options ... exportPdf: { stylesheets: [ 'path/to/fonts.css', 'path/to/editor-styles.css', 'path/to/my-styles.css' ], // More configuration of the export to PDF feature. // ... } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Thanks to this, the editor inherits the font settings and you can be sure that they will be applied in the generated PDF file as well. Take a look at another example of a `fonts.css` file. Suppose that your website uses the `Source Sans Pro` font as before but this time you want the `Inter` font to be used in the WYSIWYG editor. The file should look like this: ``` /* fonts.css */ /* Import the web font for your website. */ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700&display=swap'); /* Import the web font for your editor. */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); /* Use the Source Sans Pro web font in your website. */ html, body { font-family: "Source Sans Pro", sans-serif; /* ... */ } /* Use the Inter web font in your editor. */ .ck.ck-content.ck-editor__editable { font-family: "Inter", sans-serif; /* ... */ } /* ... */ ``` Having the file set like this for your website and just re-using it in [config.exportPdf.stylesheets](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-stylesheets) makes the whole setup as simple as possible. #### Adding a watermark to the document Apart from adding a header and a footer, there is also the possibility of adding a watermark to the generated document. This can be achieved by utilizing [config.exportPdf.dataCallback](../../api/module%5Fexport-pdf%5Fexportpdf-ExportPdfConfig.html#member-dataCallback). To do this, you need to get the data that is being sent to the converter first and update it with a watermark markup: ``` exportPdf: { // More configuration of the export to PDF. // ... dataCallback: ( editor ) => { return ` ${ editor.getData() }
Draft document
`; }, // More configuration of the export to PDF. // ... } ``` Then [update the custom CSS file](#ckeditor5/latest/features/converters/export-pdf.html--re-using-custom-editor-styling) with the proper styles: ``` .watermark { font-size: 50px; opacity: 0.5; color: black; position: fixed; left: 20%; top: 50%; transform: rotate(25deg); letter-spacing: 10px } ``` Below you can see a simplified demo with the final result. Click the toolbar button to generate the document with a watermark.
source file: "ckeditor5/latest/features/converters/export-word.html" ## Export to Word The export to Word feature lets you generate a `.docx` file directly from the editor. ### Demo The demo below lets you generate a Word file based on the editor’s content. Edit the document, then click the export to Word toolbar button to save the content as a Word file. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### How it works The export to Microsoft Word feature collects the HTML generated with the [editor.getData()](../../api/module%5Feditor-classic%5Fclassiceditor-ClassicEditor.html#function-getData) method and the [default editor content styles](#ckeditor5/latest/getting-started/setup/css.html) combined with the styles provided by you in the configuration. It then sends them to the CKEditor Cloud Services HTML to DOCX converter service. The service generates a Word file and returns it to the user’s browser so they can save it in the Word format on their disk. You can read more about the converter and the plugin in a [dedicated Feature spotlight blog post](https://ckeditor.com/blog/feature-spotlight-html-to-word-converter/). You can use the complementary [pagination feature](#ckeditor5/latest/features/pagination/pagination.html) to see where page breaks would be (after exporting your document to Word). However, due to the nature of Word page rendering, the results may be inconsistent (read more about [known issues](#ckeditor5/latest/features/pagination/pagination.html--automatic-page-breaks-in-export-to-word)). You can force the page breaks from pagination in Word by enabling the [auto\_pagination: true](../../api/module%5Fexport-word%5Fexportword-ExportWordConverterOptions.html#member-auto%5Fpagination) configuration option. You can also fine-tune the structure of the output document by using live preview. The pagination feature also shows the page count and lets you navigate between the document pages. ### Integration with merge fields (content placeholders) [Merge fields](#ckeditor5/latest/features/merge-fields.html) are visually distinct placeholder elements you can put into the content to mark places where real values should be inserted. They are perfect for creating document templates and other kinds of personalized content. This allows for automation and creating batch output of personalized `.docx` files. Learn how to configure it in the [Export to Word merge fields](#cs/latest/guides/export-to-word/merge-fields.html) guide. ### Before you start After you select a plan, follow the steps below, as explained in the [Export to Word quick start guide](#cs/latest/guides/export-to-word/quick-start.html): * [Log into the CKEditor Ecosystem customer dashboard](#cs/latest/guides/export-to-word/quick-start.html--log-in-to-the-customer-portal). * [Create the token endpoint needed for authorization](#cs/latest/guides/export-to-word/quick-start.html--creating-token-endpoint). * [Install](#ckeditor5/latest/features/converters/export-word.html--installation) and [configure](#ckeditor5/latest/features/converters/export-word.html--configuration) the CKEditor 5 export to Word plugin. ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuration #### Default configuration This is the default configuration of the Word export feature for CKEditor 5. ``` { exportWord: { fileName: 'document.docx', converterUrl: 'https://docx-converter.cke-cs.com/v2/convert/html-docx', stylesheets: [ './ckeditor5-content.css' ], converterOptions: { document: { size: 'A4', orientation: 'portrait', margin: { top: '1in', bottom: '1in', right: '1in', left: '1in', }, language: 'en' // By default it is set to editor content language. }, }, dataCallback: ( editor ) => editor.getData( { pagination: true } ) } } ``` If you are using the EU cloud region, remember to adjust the endpoint: ``` exportWord: { converterUrl: 'https://docx-converter.cke-cs-eu.com/v2/convert/html-docx' } ``` #### `stylesheets` option Use the `stylesheets` option to provide paths (relative or absolute URLs) to all style sheets that should be included during the HTML to DOCX conversion. The rule of thumb is if you want the export to preserve the styles, always add the style sheets with the content styles of the editor. Their path depends on your application setup, for example: ``` { exportWord: { stylesheets: [ './ckeditor5-content.css' './styles.css' ], // ... } } ``` In the snippet above, we assume both style sheets are available via the relative path on the client side. For example, some frameworks allow to place files in the `public` folder. #### Plugin options For some use cases the default configuration will suffice. As you can see in the example above, you can improve how your Word file will look by adjusting the Word export plugin configuration. * **[config.exportWord.stylesheets](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-stylesheets)** You can set the paths (relative or absolute URLs) to the style sheets that should be included during the HTML to DOCX conversion. * **[config.exportWord.fileName](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-fileName)** Sets the name for the generated Word file (together with the extension). The default name is `document.docx`. You can see it called in the [default configuration](#ckeditor5/latest/features/converters/export-word.html--default-configuration) listing above. This option, however, also allows for using a callback to generate a dynamic file name. In the example below, the document’s title will be used as the file name of the generated `.docx` file. ``` // Dynamic file name. const exportWordConfig = { fileName: () => { const articleTitle = document.querySelector( '#title' ); return `${ articleTitle.value }.docx`; } } ``` * **[config.exportWord.converterUrl](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-converterUrl)** By default, the Word export feature uses the CKEditor Cloud Services HTML to DOCX converter service to generate the Word files. You can use this option to provide the URL to an on-premises converter. [Contact us](https://ckeditor.com/contact/) if you need this feature. * **[config.exportWord.tokenUrl](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-tokenUrl)** A token URL or a token request function. This field is optional and you should use it when you require a different [tokenUrl](../../api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-tokenUrl) for the export to Word feature. You can skip this option if you use the `cloudServices` configuration to provide the same `tokenUrl`. In most cases you will probably want to provide the token in `cloudServices`, as other plugins like [real-time collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration.html) will use this token as well. In this guide, to explicitly show that this value is needed, we leave it inside the `exportWord` configuration. * **[config.exportWord.converterOptions](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-converterOptions)** The plugin allows you to provide a custom [CKEditor Cloud Services HTML to DOCX converter configuration](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-converterOptions), such as paper size, orientation, or watermark. Below, you will find the options listed in a sample configuration: ``` converterOptions: { document: { size: 'A4', margin: { top: '20mm', bottom: '20mm', right: '12mm', left: '12mm' } }, watermark: { source: 'https://placehold.co/600x400/transparent/DDD?text=Watermark', width: '600px', height: '400px', washout: 'true' } } ``` * **[config.exportWord.dataCallback](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-dataCallback)** By default, the plugin uses `editor.getData( { pagination: true } )` to gather the HTML sent to the conversion service. You can use this option to customize the editor’s data. When using the [pagination](#ckeditor5/latest/features/pagination/pagination.html) feature, the `pagination:true` option inserts additional markers into the editor’s data. Thanks to that, the HTML to DOCX converter creates a Word document similar to what is displayed in the editor. #### Export to Word V1 **Note:** Export to Word uses the new version of the converter by default, the old one will no longer receive updates. It is highly recommended to migrate to the [latest version](https://docx-converter.cke-cs.com/v2/convert/docs). For more details on migrating from `v1` to `v2` see the [migration guide](https://docx-converter.cke-cs.com/v2/convert/docs#section/Export-to-Word/Migrating-from-v1). To use `v1`, you need to specify the version in the `version` property of the configuration, as shown in the snippet below. ``` { exportWord: { // ... version: 1, // by default this is set to 2. converterOptions: { format: 'A4', orientation: 'portrait', margin_top: '1in', margin_bottom: '1in', margin_right: '1in', margin_left: '1in' }, // ... } } ``` View V1 headers and footers example ``` // Let's keep the CSS string as a variable to avoid unnecessary string duplication. const templateCSS = '.styled { color: #4b22aa; text-align: center; }' const converterOptions = { header: [ // Header template for all headers (without the `type` property). { html: '

Default header content

', css: templateCSS }, // Header template only for the first page of the document. { html: '

First document page header content

', css: templateCSS, type: 'first' }, // Header template for every even page of the document. { html: '

Every even page header content

', css: templateCSS, type: 'even' }, // Header template for every odd page of the document. { html: '

Every odd page header content

', css: templateCSS, type: 'odd' } ], footer: [ // Footer template for all footers (without the `type` property). { html: '

Default footer content

', css: templateCSS }, // Footer template only for the first page of the document. { html: '

First document page footer content

', css: templateCSS, type: 'first' }, // Footer template for every even page of the document. { html: '

Every even page footer content

', css: templateCSS, type: 'even' }, // Footer template for every odd page of the document. { html: '

Every odd page footer content

', css: templateCSS, type: 'odd' } ], } ```
### HTML to Word converter features #### Styling a document By default, the [export to Word](../../api/module%5Fexport-word%5Fexportword-ExportWord.html) plugin takes editor content styles and sends them to the CKEditor Cloud Services HTML to DOCX Converter. You can also add custom styles by providing the paths to the external CSS files. ``` // More editor's configuration. // ... exportWord: { fileName: 'document.docx', converterUrl: 'https://docx-converter.cke-cs.com/v2/convert/html-docx', stylesheets: [ 'path/to/editor-styles.css', 'path/to/my-styles.css' ], // More configuration of the export to Word feature. // ... } // More editor's configuration. // ... ``` ##### Supported CSS properties The HTML to DOCX Converter supports proper CSS inheritance with a set of whitelisted properties. You can use them to style the document content. You can apply CSS properties like: * `color` * `background-color` * `font-size` * `font-family` * `text-align` to the following elements: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `p`, `span`, `td`, `th`, `strong`, `i`, `u`, `s`, `sub`, `sup`, `mark`. You can also position images using the `float` CSS property, supporting `left`, `right`, and `none` values. #### Setting the page format Consistency is an important factor. To make sure that the editor content and the generated Word file look the same, you need to match their format settings. You can change your existing style sheet or use a new one, for example, `format.css`. By default, the CKEditor Cloud Services HTML to DOCX converter is set to A4 format, but you may change this setting in your configuration. Assuming that you want to create a document in the US Letter format, with the standard margins (`19mm` for each side), here is the example code you can use: ``` ClassicEditor .create( { // ... Other configuration options ... exportWord: { stylesheets: [ 'path/to/editor-styles.css', 'path/to/my-styles.css' ], fileName: 'my-document.docx', converterOptions: { document: { size: 'Letter', // Document format settings with proper margins. margin: { top: '19mm', bottom: '19mm', right: '19mm', left: '19mm' } } } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` Now set the corresponding editor styles: ``` /* format.css */ /* Styles for the editable. */ .ck.ck-content.ck-editor__editable { /* US Letter size. */ width: 215.9mm; /* Padding is your document's margin. */ padding: 19mm; /* You don't want to change the size of the editor by applying the new padding values. */ box-sizing: border-box; /* ... */ } ``` With these settings, the content in the generated Word file should have the same US Letter format layout as it has in the editor. #### Header and footer The converter lets you set the document’s header and footer similarly to Microsoft Word or Google Docs. ``` const templateCSS = '.styled { color: #4b22aa; text-align: center; }' const converterOptions = { headers: { // Header template for all headers. default : { html: '

Default header content

', css: templateCSS }, // Header template only for the first page of the document. first: { html: '

First document page header content

', css: templateCSS }, // Header template for every even page of the document. even: { html: '

Every even page header content

', css: templateCSS }, // Header template for every odd page of the document. odd: { html: '

Every odd page header content

', css: templateCSS } }, footers: { // Footer template for all footers. default: { html: '

Default footer content

', css: templateCSS }, // Footer template only for the first page of the document. first: { html: '

First document page footer content

', css: templateCSS }, // Footer template for every even page of the document. even: { html: '

Every even page footer content

', css: templateCSS }, // Footer template for every odd page of the document. odd: { html: '

Every odd page footer content

', css: templateCSS } }, } ``` There are only the `headers` and `footers` objects which contain other objects where the key is a `type` of your header/footer. Regarding CSS, you can style the `headers` and `footers` using the same [supported properties](#ckeditor5/latest/features/converters/export-word.html--supported-css-properties) as used for styling the whole document. For more details, refer to the [CKEditor Cloud Services HTML to DOCX converter’s documentation](https://docx-converter.cke-cs.com/docs#section/Options). As you can see, the `headers` and `footers` options take an array of objects that define templates for the particular type of page. If you want to have consistent templates no matter the page, you can define only the `default` `headers/footers` template (**Note:** This setting misses the `type` property on purpose). If you import headers and footers from a Word document and want to preserve and reapply them when exporting, see the [preserving headers and footers](#ckeditor5/latest/features/converters/import-word/import-word.html--preserving-headers-and-footers) section in the [Import from Word](#ckeditor5/latest/features/converters/import-word/import-word.html) guide.
#### Setting the base URL To enable proper resolution of relative URLs for images and links, you need to pass the `base_url` option: ``` const conversionOptions = { base_url: 'https://ckeditor.com' }; ``` For an editor’s content like the one below: ```

Our company homepage

Our logo

``` this option will result in resolving these URLs into their absolute forms: * `/` will become `https://ckeditor.com`, * `/logo.svg` will become `https://ckeditor.com/logo.svg`. #### Adding a watermark The Export to Word converter allows for easy adding a graphic watermark via the [converterOptions](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html#member-converterOptions) configuration setting. The watermark configuration is represented as an object containing 4 properties: * `source`: A source of the image used for the watermark. * `width`: A string value representing the width of the watermark. * `height`: A string value representing the height of the watermark. * `washout`: Determines whether the washout effect should be applied. Optional - the default value is `false`. ``` converterOptions: { watermark: { source: 'https://placehold.co/600x400/EEE/31343C', width: '600px', height: '400px', washout: 'true' } } ``` #### Comments and suggestions When your editor has [collaboration features](https://ckeditor.com/collaboration/) (like comments and track changes) enabled, the [export to Word](../../api/module%5Fexport-word%5Fexportword-ExportWord.html) feature will take care of setting the configuration needed by the CKEditor Cloud Services HTML to DOCX converter. But if for some reason you need to pass your own data, you can do this via the [REST API converter options](https://docx-converter.cke-cs.com/docs#section/Options). #### Other * By default, the generated Word file is encoded with **`UTF-8`**. ### Known issues Not all CKEditor 5 plugins and features are compatible with export to Word at the moment. Feel free to [contact us](https://ckeditor.com/contact/) if you are interested in any of these features specifically. Here is a list of known issues: #### Automatic page breaks with the pagination feature Browser engines and Microsoft Word differ significantly. Because of that, the automatic prediction of page breaks provided by the [pagination feature](#ckeditor5/latest/features/pagination/pagination.html) in [Export to Word](#ckeditor5/latest/features/converters/export-word.html) is problematic and error-prone. We recommend reviewing your document’s structure during the exporting and manually applying the page breaks to maintain the preferred structure. If you still want to enforce the page breaks, set the `auto_pagination: true` option in the Export to Word configuration. You can also use [Export to PDF](#ckeditor5/latest/features/converters/export-pdf.html), where predicting page breaks is more straightforward and works more consistently. #### Unsupported plugins * [Media embed](#ckeditor5/latest/features/media-embed.html) – Embedded media will not be included in the exported document. * [MathType](#ckeditor5/latest/features/math-equations.html) – Supported partially. The plugin parses the data but not the formatting, losing some of the math operators and not reproducing a usable equation in the effecting file. #### Unsupported features * Inline and block formatting [suggestions](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) – Such suggestions are not included in the exported document. * [Comments](#ckeditor5/latest/features/collaboration/comments/comments.html) applied to whole widgets (like tables) – Such comments are not included in the exported document. ### Related features * The complementary [pagination feature](#ckeditor5/latest/features/pagination/pagination.html) provides live preview of the document’s page breaks, ensuring the output document looks correct. * If you would like to export your content to a portable, universal format, using the [export to PDF](#ckeditor5/latest/features/converters/export-pdf.html) feature will allow you to generate PDF files out of your editor-created content. ### Common API The [ExportWord](../../api/module%5Fexport-word%5Fexportword-ExportWord.html) plugin registers: * The `'exportWord'` button. * The `'exportWord'` command implemented by [ExportWordCommand](../../api/module%5Fexport-word%5Fexportwordcommand-ExportWordCommand.html). You can execute the command using the [editor.execute()](../../api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-execute) method. However, if you want to use the command directly (not via the toolbar button), you need to specify the [the options](../../api/module%5Fexport-word%5Fexportword-ExportWordConfig.html) or gather them from the configuration. Otherwise, the command will not execute properly. The example code to use the command directly should look like this: ``` // Start generating a Word file based on the editor content and the plugin configuration. const config = editor.config.get( 'exportWord' ); editor.execute( 'exportWord', config ); ``` ### REST API The HTML to DOCX converter provides an API for converting HTML documents to Microsoft Word `.docx` files. Read the [REST API documentation](https://docx-converter.cke-cs.com/v2/convert/docs#section/Export-to-Word) to find out how to employ it in your implementation. source file: "ckeditor5/latest/features/converters/import-word/features-comparison.html" ## Import from Word vs paste from Office comparison In addition to import from Word, there is a simpler [paste from Office](#ckeditor5/latest/features/pasting/paste-from-office.html) plugin that lets you paste content from Microsoft Word while maintaining its original structure and formatting. However, these two solutions differ in many ways. ### Paste from Office The [paste from Office](#ckeditor5/latest/features/pasting/paste-from-office.html) and [paste from Office enhanced](#ckeditor5/latest/features/pasting/paste-from-office-enhanced.html) features allow you to paste content from Microsoft Word into your CKEditor 5 WYSIWYG editor and maintain the original structure and formatting. After creating a document in Microsoft Word you can copy it to CKEditor 5 and retain basic text styling, heading levels, links, lists, tables, and images – as long as these features are supported by the editor itself. * Suitable for small documents and for use cases in which only parts of the document are selected and copied. * Relies on OS Clipboard HTML, which limits the number of supported features. * Preserves the original DOCX formatting that was selected and copied by hand. * Simple, intuitive, but cannot be used to automate the migration process of many Word documents via the REST API. This operation is fast and easy, but can only be done manually. ### Import from Word The import from Word service can be automated and does not require the presence of the WYSIWYG editor, nor human supervision to convert files. Compared to paste from Office, import can work with any [content formatting](#cs/latest/guides/import-from-word/content-formatting.html) and is not limited by features supported by the editor. * Allows for converting large documents into HTML that can be easily imported to CKEditor 5 and other tools. * Operates directly on XML, which includes more information about the document and Word instance settings. * Available as a CKEditor 5 plugin and as a REST API for a direct server-to-server conversion. * Available both as a SaaS service and as an on-premises solution. * Suitable for migration of the whole database of Word documents to HTML via a REST API service. * Perfect solution for more advanced documents that need to be edited or displayed in the browser. * Supports collaboration features like track changes and comments out of the box. ### Features comparison The following tables compare the features of the paste from Office and paste from Office enhanced CKEditor 5 plugin and the import from Word feature. For a more detailed import from Word features overview, refer to the [content formatting](#cs/latest/guides/import-from-word/content-formatting.html) guide. #### Collaboration features | Feature name | Paste | Enhanced | Import | | ------------------------------- | ----- | -------- | ------ | | Comments | ❌ | ❌ | ✅ | | Comments archive | ❌ | ❌ | ✅ | | Comments - images | ❌ | ❌ | ✅ | | Comments - table cells | ❌ | ❌ | ✅ | | Track changes - text insertion | ❌ | ❌ | ✅ | | Track changes - text deletion | ❌ | ❌ | ✅ | | Track changes - move text | ❌ | ❌ | ✅ | | Track changes - images | ❌ | ❌ | ✅ | | Track changes - tables | ❌ | ❌ | ⚠️ | | Track changes - table text | ❌ | ❌ | ✅ | | Track changes - table rows | ❌ | ❌ | ⚠️ | | Track changes - table cells | ❌ | ❌ | ⚠️ | | Track changes - lists | ❌ | ❌ | ⚠️ | | Track changes - list text | ❌ | ❌ | ✅ | | Track changes - list items | ❌ | ❌ | ⚠️ | | Track changes - text formatting | ❌ | ❌ | ⚠️ | * ⚠️ Import: Track changes for unsupported features will preserve the original content of the author’s suggestion. However, they will not be recognized as proper track changes suggestions. As an example, if a user adds a table using track changes, the table will be output in HTML, but it will not be marked as a suggestion. That limitation is going to be fixed soon, with upcoming import From Word releases. #### Inline formatting | Feature name | Paste | Enhanced | Import | | ---------------- | ----- | -------- | ------ | | Font color | ✅ | ✅ | ✅ | | Font background | ✅ | ✅ | ✅ | | Font size | ✅ | ✅ | ✅ | | Font family | ✅ | ✅ | ✅ | | Bold | ✅ | ✅ | ✅ | | Italics | ✅ | ✅ | ✅ | | Underline | ✅ | ✅ | ✅ | | Underline custom | ❌ | ⚠️ | ✅ | | Strike-through | ✅ | ✅ | ✅ | | Subscript | ✅ | ✅ | ✅ | | Superscript | ✅ | ✅ | ✅ | | Link | ✅ | ✅ | ✅ | | Soft line break | ✅ | ✅ | ✅ | | Small caps | ❌ | ✅ | ✅ | | All caps | ❌ | ✅ | ✅ | | Letter spacing | ⚠️ | ✅ | ✅ | | Font stretching | ❌ | ✅ | ✅ | | Hidden text | ⚠️ | ⚠️ | ✅ | * ⚠️ Paste: Letter spacing and hidden text are only supported with the [General HTML support](#ckeditor5/latest/features/html/general-html-support.html) feature enabled. * ⚠️ Paste enhanced: Advanced underline is pasted as regular underline. * ⚠️ Paste enhanced: Hidden text is only supported with the [General HTML support](#ckeditor5/latest/features/html/general-html-support.html) feature enabled. #### Paragraphs | Feature name | Paste | Enhanced | Import | | ---------------------- | ----- | -------- | ------ | | Text alignment | ✅ | ✅ | ✅ | | Indentation | ✅ | ✅ | ✅ | | First line indentation | ⚠️ | ⚠️ | ✅ | | Hanging indentation | ⚠️ | ⚠️ | ✅ | | Line height | ⚠️ | ⚠️ | ✅ | | Paragraph spacing | ⚠️ | ⚠️ | ✅ | | Paragraph borders | ⚠️ | ⚠️ | ✅ | | Background color | ⚠️ | ⚠️ | ✅ | * ⚠️ First line indentation, hanging indentation, line height, paragraph spacing, and paragraph borders are only supported with the [General HTML support](#ckeditor5/latest/features/html/general-html-support.html) feature enabled. #### Headings | Feature name | Paste | Enhanced | Import | | ---------------------------------- | ----- | -------- | ------ | | Built-in heading styles | ✅ | ✅ | ✅ | | Preservation of heading formatting | ❌ | ✅ | ✅ | | Custom outline level | ❌ | ✅ | ✅ | #### Lists | Feature name | Paste | Enhanced | Import | | -------------------------------------- | ----- | -------- | ------ | | Ordered lists | ✅ | ✅ | ✅ | | Unordered lists | ✅ | ✅ | ✅ | | Custom list markers | ❌ | ️❌ | ❌ | | Ordered list language-specific markers | ❌ | ❌ | ✅ | | Custom start number | ✅ | ✅ | ✅ | | Different start number in the middle | ❌ | ❌ | ✅ | | Multi-level list | ❌ | ❌ | ⚠️ | * ⚠️ Import: Support for multi-level lists with X level of tabulation shifting is now available. Please be aware that marker continuation from previous levels (for example, 2.1, 2.2) is not supported for now. #### Tables | Feature name | Paste | Enhanced | Import | | --------------------------- | ----- | -------- | ------ | | Table width | ✅ | ✅ | ✅ | | Cell/column width | ✅ | ✅ | ✅ | | Cell/row height | ✅ | ✅ | ✅ | | Cell merging | ✅ | ✅ | ✅ | | Cell padding | ✅ | ✅ | ✅ | | Cell spacing | ✅ | ✅ | ✅ | | Cell’s horizontal alignment | ✅ | ✅ | ✅ | | Cell’s vertical alignment | ✅ | ✅ | ✅ | | Table background color | ✅ | ✅ | ✅ | | Cell background color | ✅ | ✅ | ✅ | | Table border style | ✅ | ✅ | ✅ | | Table border color | ✅ | ✅ | ✅ | | Cell border style | ✅ | ✅ | ✅ | | Cell border color | ✅ | ✅ | ✅ | | Table header | ✅ | ✅ | ✅ | | Nested tables | ✅ | ✅ | ✅ | | Table alignment/floating | ✅ | ✅ | ✅ | | Table caption | ⚠️ | ⚠️ | ⚠️ | * ⚠️ Table caption is converted to a styled (only import) paragraph. #### Images | Feature name | Paste | Enhanced | Import | | ---------------------------- | ----- | -------- | ------ | | Embedded images | ✅ | ✅ | ✅ | | External images | ✅ | ✅ | ✅ | | Image link | ✅ | ✅ | ✅ | | Image alternative text | ✅ | ✅ | ✅ | | Image height | ✅ | ✅ | ✅ | | Image width | ✅ | ✅ | ✅ | | Image alignment | ✅ | ✅ | ✅ | | Absolutely positioned images | ⚠️ | ⚠️ | ⚠️ | | Image caption | ⚠️ | ⚠️ | ⚠️ | * ⚠️ Absolutely positioned images are retained, but their original position is lost. * ⚠️ Image caption is converted to a styled (import only) paragraph. #### Page breaks | Feature name | Paste | Enhanced | Import | | ----------------------- | ----- | -------- | ------ | | Normal page break | ✅ | ✅ | ✅ | | Page break before style | ❌ | ❌ | ✅ | #### Horizontal lines | Feature name | Paste | Enhanced | Import | | --------------- | ----- | -------- | ------ | | Horizontal line | ✅ | ✅ | ✅ | #### Word styles | Feature name | Paste | Enhanced | Import | | ---------------- | ----- | -------- | ------ | | Built-in styles | ❌ | ⚠️ | ✅ | | Format styles | ❌ | ⚠️ | ✅ | | Font styles | ❌ | ⚠️ | ✅ | | Paragraph styles | ❌ | ⚠️ | ✅ | | Border styles | ❌ | ⚠️ | ✅ | | Numbering styles | ✅ | ✅ | ✅ | * ⚠️ Paste enhanced: Styles are only supported with the [General HTML support](#ckeditor5/latest/features/html/general-html-support.html) feature enabled. #### Default styles Default styles require enabling the `config.default_styles` configuration option, both for the CKEditor 5 import from Word plugin and the REST API. | Feature name | Paste | Enhanced | Import | | ---------------- | ----- | -------- | ------ | | Format styles | ⚠️ | ✅ | ✅ | | Font styles | ⚠️ | ✅ | ✅ | | Paragraph styles | ⚠️ | ✅ | ✅ | * ⚠️ Paste: Default document styles are partially supported, but cannot be disabled or enabled on purpose. #### Sections | Feature name | Paste | Enhanced | Import | | ------------------------ | ----- | -------- | ------ | | Document margins | ❌ | ✅ | ✅ | | Document size | ❌ | ✅ | ✅ | | Multi-sectioned document | ❌ | ❌ | ❌ | | Section columns | ❌ | ❌ | ❌ | | Headers and footers | ❌ | ❌ | ❌ | * ⚠️ Import: Multi-sectioned document and section columns are currently not supported. #### Complex objects | Feature name | Paste | Enhanced | Import | | ----------------- | ----- | -------- | ------ | | Table of contents | ⚠️ | ✅ | ✅ | | Form objects | ⚠️ | ⚠️ | ⚠️ | * ⚠️ Paste: Only the table of contents text is preserved, but the structure and styling are lost. * ⚠️ Form objects: Only text and styling are retained. ### Technical details To better understand the differences between these two products, it is worth learning how both features work on a technical level. In paste from Office, the editor uses the operating system’s clipboard, which is fed with Microsoft Word content in HTML upon copying it from the document. That HTML is delivered by the Word application itself and includes the essential formatting of the document. When the user pastes something from a Word document, CKEditor 5 cleans that HTML up and makes it semantically correct, so it can be consumed by the editor. However, that operation has its limitations, as CKEditor 5 is only able to understand as much content as it gets from the clipboard and depends entirely on the clipboard implementation of the Microsoft Word application. The import from Word feature does not have this limitation. As it has direct access to the document, it can retrieve as much information from the document as Word. Therefore, it is possible to support things like collaboration features, document settings, and others, which would not be available when operating on clipboard content only. We are no longer limited by the Microsoft Word application and thanks to that, import from Word produces a more advanced HTML than Paste from Word. ### List of CSS properties that require GHS This is a list of all CSS properties that are properly converted by the [import from Word](#ckeditor5/latest/features/converters/import-word/import-word.html) feature, but will not work by default in CKEditor 5 and require the [General HTML Support](#ckeditor5/latest/features/html/general-html-support.html) (GHS) feature. #### Paragraphs ##### HTML elements: * `p` ##### CSS properties: * `background-color` * `line-height` * `border-top` * `border-bottom` * `border-left` * `border-right` * `margin-top` * `margin-bottom` * `text-indent` #### Headings ##### HTML elements * `h1` * `h2` * `h3` * `h4` * `h5` * `h6` ##### CSS properties: * `font-weight` * `font-size` Headings also require all CSS properties that paragraphs do. #### Lists ##### HTML elements: * `ul` * `ol` ##### CSS properties: * `list-style-type` * `margin-top` * `margin-bottom` #### List items ##### HTML elements: * `li` ##### CSS properties: * `list-style-type` #### Spans ##### HTML elements: * `span` ##### CSS properties: * `letter-spacing` * `text-transform` * `font-variant-caps` * `font-stretch` #### Underlines ##### HTML elements: * `u` ##### CSS properties: * `text-decoration-line` * `text-decoration-style` * `text-decoration-thickness` * `text-decoration-color` * `text-decoration-skip-ink` #### Images ##### HTML elements: * `img` ##### CSS properties: * `position` * `display` * `float` * `margin-top` * `margin-left` * `margin-right` * `transform` * `z-index` #### Figures ##### HTML elements: * `figure` ##### CSS properties: * `margin-top` * `margin-bottom` * `margin-left` * `margin-right` #### Tables ##### HTML elements: * `table` ##### CSS properties: * `border-collapse` * `border-spacing` #### Table cells ##### HTML elements: * `td` * `th` ##### CSS properties: * `vertical-align` #### Table header cells ##### HTML elements: * `th` ##### CSS properties: * `font-weight` * `text-align` source file: "ckeditor5/latest/features/converters/import-word/import-word.html" ## Import from Word The import from Word feature lets you import `.docx` (Word document) or `.dotx` (Word template) files into the editor. The process preserves formatting and rich media as well as [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) and [tracked changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) (if these features are enabled). ### Demo The demo below lets you import a Word file into the editor. To test the feature, download the [sample Word document](../../../assets/pd%5Fpolicy.docx). Use the import from Word toolbar button and select the downloaded file. The file’s content will appear in the editor. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Additional feature information The import from Word feature sends the selected Word file to the CKEditor Cloud Services DOCX to HTML converter service. The service returns HTML code generated from the uploaded file and then inserts it into the editor content in place of the document selection. Even though CKEditor 5 offers a dedicated [pagination plugin](#ckeditor5/latest/features/pagination/pagination.html), it cannot be used to reflect the original page division in content imported from Word. This is an export-only feature. ### Importing styles CKEditor 5 supports two strategies for importing styles from Word: 1. Using styles defined in CKEditor 5. 2. Using styles defined in Word. The decision as to which approach to use is strongly related to your use case. The ability to choose whether to retain or to drop native Word styles gives you great flexibility. It also allows you to tailor the service to your specific needs. #### Using styles defined in CKEditor The import from Word feature is pre-configured to preserve styles defined in the CKEditor 5’s implementation or applied directly by the end-user. This means that it will retain basic text styling (like bold or italics), headings, images, tables, and the overall document structure. At the same time, it will allow the editor to apply styles used by CKEditor 5 to the imported content for more general formatting, like font family, font size, paragraph spacing, etc. if not set. This way you import the file content into the editor and it does not differ visually from the already existing content. This is useful when the formatting needs to follow corporate guidelines or a brand book. With that approach, import keeps semantically close formatting to the existing content. #### Using styles defined in Microsoft Word Word allows you to change the default formatting of documents to style them in a specific way. You can apply the same style to the entire Word document without needing to do it manually. For example, you can open the Format menu, choose the Font option, and then change some formatting, such as font size and font family. The behavior of that Word feature highly impacts how the document will be styled after the conversion. Therefore, instead of always applying the default styles, the converter can enable importing the entire Word document styling. In this approach, the Word default content formatting set for the whole document is all included in the import and preserved as much as possible (within the support of CKEditor 5 feature plugins). Word [applies styles in a specific way](#cs/latest/guides/import-from-word/styles.html--default-styles), and the user may choose to retain these. To make the converter work this way, set the [FormattingOptions](../../../api/module%5Fimport-word%5Fimportword-ImportWordFormattingOptions.html). This approach is most useful when users want to edit a Word document directly in their browser and need cross-platform interoperability. You can call it the “complete Word editing experience.” However, an integrator can still choose which styles to preserve by configuring CKEditor 5 appropriately. A sample configuration for this option may look similar to this one: ``` importWord: { formatting: { // Keep only very minimal styling in comments: comments: 'basic', // Do not include Word default document styles (e.g. default font / size): defaults: 'none', // Include resets so that HTML heading tags reflect Word heading defaults resets: 'inline', // Preserve Word-defined styles inline: styles: 'inline' } } ``` ### Paste from Office vs import from Word The [paste from Office](#ckeditor5/latest/features/pasting/paste-from-office.html) feature allows you to paste content from Microsoft Word into your CKEditor 5 WYSIWYG editor and maintain the original structure and formatting. After creating a document in Microsoft Word, you can copy it to CKEditor 5 and retain basic text styling, heading levels, links, lists, tables, and images if these features are supported by the installed CKEditor 5 plugins. For example, if font colors are not explicitly turned on in the editor instance, they will be dropped. This operation is fast and easy, but can only be done manually. [Import from Word](#cs/latest/guides/import-from-word/overview.html), however, is much more advanced. First of all, you can automate it and it does not need the presence of CKEditor 5 editor or human supervision to convert files. You can feed the files into the service and convert them automatically, as part of a larger process. While the paste from Office feature can only retain the formatting supported by the editor instance, this limitation does not concern the import from Word service. You can read more about the differences and specific supported features in the [dedicated comparison guide](#ckeditor5/latest/features/converters/import-word/features-comparison.html). ### Automatic content filtering Due to the CKEditor 5 [custom data model](#ckeditor5/latest/framework/index.html), the editor will only preserve content handled by its plugins. This guarantees that the output provided by CKEditor 5 will be clean and semantic. However, this also means that you may need to enable some additional plugins in your rich-text editor to prevent stripping content or formatting (for example, the [font family and font size](#ckeditor5/latest/features/font.html) features to handle font formatting). ### `base64` images and Content Security Policy If you use the import from Word plugin without custom uploaders, Content Security Policy (CSP) may prevent `base64` images from being imported due to security concerns. This will result in an error in the console like this one: ``` Refused to connect to `data:image/jpeg:base64,/xxxxx utills.js:43 xxxxx` because it violates the documents Content Security Policy. ``` In such a case, you should try using a ready-made upload solution like [CKBox](#ckeditor5/latest/features/file-management/ckbox.html). You can also consider changing the CSP directive. ### Comments and tracked changes The import from Word feature supports Word files with comments and tracked changes. They will be imported as long as your CKEditor 5 preset includes the [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) and [track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) features. All comments and suggestions imported from a Word file will use the same author’s name as in the Word file. They will also include a special label informing that a given item comes from an external source. Read more about the integration between the import from Word and the comments feature in the [Comments walkthrough](#ckeditor5/latest/features/collaboration/comments/comments-walkthrough.html--import-from-word) guide. If your CKEditor 5 preset does not include the track changes feature, the content will be imported as if all tracked changes were accepted (same as “No Markup” displayed mode). ### Merge fields (content placeholders) The import from Word feature will recognize Word document merge fields and will seamlessly convert them to the [editor merge fields](#ckeditor5/latest/features/merge-fields.html). ### Before you start You can try the import from Word feature in preview mode without a valid license. It will import a part of the document, and replace the rest of the content with a “lorem ipsum” text placeholder. After you purchase a license, follow the steps below, as explained in the [Import from Word quick start guide](#cs/latest/guides/import-from-word/quick-start.html): * [Log into the CKEditor Ecosystem customer dashboard](#cs/latest/guides/import-from-word/quick-start.html--log-in-to-the-customer-portal). * [Create the token endpoint needed for authorization](#cs/latest/guides/import-from-word/quick-start.html--creating-token-endpoint). ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: ### Configuration You can configure the feature via the [config.importWord](../../../api/module%5Fimport-word%5Fimportword-ImportWordConfig.html) object. #### Providing the token URL The import from Word feature requires the token endpoint URL configured in the [config.importWord.tokenUrl](../../../api/module%5Fimport-word%5Fimportword-ImportWordConfig.html#member-tokenUrl) key. If not explicitly provided, the token URL from [config.cloudServices.tokenUrl](../../../api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-tokenUrl) is used instead. If both are provided, the token URL defined in `config.importWord.tokenUrl` takes precedence over the `config.cloudServices.tokenUrl`. ``` ClassicEditor .create( { // ... Other configuration options ... importWord: { tokenUrl: 'https://example.com/cs-token-endpoint' } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Configuring the converter service URL Below is the default configuration of the Import from Word feature for CKEditor 5 with Cloud Services. ``` ClassicEditor .create( { // ... Other configuration options ... importWord: { converterUrl: 'https://docx-converter.cke-cs.com/v2/convert/docx-html' } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` If you are using the EU cloud region, remember to adjust the endpoint: ``` importWord: { converterUrl: 'https://docx-converter.cke-cs-eu.com/v2/convert/docx-html' } ``` If the service is hosted in your own environment, you should configure the converter service URL via the [config.importWord.converterUrl](../../../api/module%5Fimport-word%5Fimportword-ImportWordConfig.html#member-converterUrl) option: ``` ClassicEditor .create( { // ... Other configuration options ... importWord: { converterUrl: 'https://example.com/converter' } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Default styles By default, the converter service will convert styles explicitly applied to the content. You can change this behavior by passing the [config.importWord.formatting](../../../api/module%5Fimport-word%5Fimportword-ImportWordConfig.html#member-formatting) object: ``` ClassicEditor .create( { // ... Other configuration options ... importWord: { formatting: { resets: 'none', defaults: 'none', styles: 'inline' } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` #### Comments styles If the imported document contains comments, only their basic styles will be kept by default. However, you can change this behavior by passing the [config.importWord.formatting.comments](../../../api/module%5Fimport-word%5Fimportword-ImportWordFormattingOptions.html#member-comments) option: ``` ClassicEditor .create( { // ... Other configuration options ... importWord: { formatting: { comments: 'none' } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` This configuration option can take the following values: * `'basic'` – Only basic styles are kept (bold, italic, underline, strikethrough and links). * `'none'` – Comment text is imported without any styling. * `'full'` – All styles are preserved (not recommended). ### Preserving headers and footers While CKEditor 5 currently focuses on editing the document body, you can preserve headers and footers during the import process to ensure a lossless round-trip (Import Word → CKEditor 5 → Export Word). The converter service returns header and footer data in the response. You can intercept this data using the [dataInsert](../../../api/module%5Fimport-word%5Fimportwordcommand-ImportWordCommand.html#event-dataInsert) event, store it (for example, in dedicated model roots), and then reapply it when exporting the document using the [export to Word](#ckeditor5/latest/features/converters/export-word.html) feature. The following example plugin demonstrates how to: 1. Capture headers and footers from the import response. 2. Store them in separate model roots. Note the use of `undoStepBatch` to ensure that the user can undo the entire import operation in a single step. 3. Inject the stored data back into the configuration when the `exportWord` command is executed. ``` import { Plugin } from 'ckeditor5'; import { ImportWordEditing } from 'ckeditor5-premium-features'; class StoreHeadersAndFootersPlugin extends Plugin { static get pluginName() { return 'StoreHeadersAndFootersPlugin'; } static get requires() { return [ ImportWordEditing ]; } init() { const importCommand = this.editor.commands.get( 'importWord' ); this.listenTo( importCommand, 'dataInsert', ( _, { headers, footers, undoStepBatch } ) => { undoStepBatch ??= importCommand._undoStepBatch; this._processSection( undoStepBatch, 'header', headers ?? {} ); this._processSection( undoStepBatch, 'footer', footers ?? {} ); } ); } afterInit() { const exportCommand = this.editor.commands.get( 'exportWord' ); if ( exportCommand ) { this.listenTo( exportCommand, 'execute', ( _, [ data ] ) => { data.dataCallback = () => this.editor.getData({ rootName: 'content' }); data.converterOptions = { ...data.converterOptions ?? {}, headers: this._getExportDataForSection( 'header' ), footers: this._getExportDataForSection( 'footer' ) }; }, { priority: 'high' } ); } } /** * Iterates over section variants (default, first, odd, even) and updates roots. */ _processSection( batchType, type, definitions ) { const { model, data } = this.editor; for ( const [ variant, content ] of Object.entries( definitions ) ) { const rootName = `${ type }:${ variant }`; model.enqueueChange( batchType, writer => { if ( !model.document.getRoot( rootName )?.isAttached() ) { writer.addRoot( rootName, '$root' ); } } ); data.set( { [ rootName ]: content.html }, { suppressErrorInCollaboration: true, batchType, } ); } } /** * Gathers export data for headers or footers. */ _getExportDataForSection( type ) { const exportData = Object.create( null ); for ( const variant of [ 'default', 'first', 'odd', 'even' ] ) { const rootName = `${ type }:${ variant }`; const root = this.editor.model.document.getRoot( rootName ); if ( root?.isAttached() ) { const html = this.editor.getData( { rootName } )?.trim(); if ( html ) { exportData[ variant ] = { html }; } } } return exportData; } } ``` For more details, visit the [ckeditor5-collaboration-samples](https://github.com/ckeditor/ckeditor5-collaboration-samples/) repository, which contains a complete example of how to implement this feature. #### Making headers and footers editable Since the implementation above stores headers and footers in **separate model roots** (e.g., `header:default`, `footer:first`), they become fully functional parts of the editor model. It means they are not just static HTML strings. You can render them in your application interface and bind them to the editor view, allowing users to **edit headers and footers** directly. The recommended approach is to use the [Multiroot editor](#ckeditor5/latest/examples/builds/multi-root-editor.html) or manually create editable UI views for these specific roots in a [Decoupled editor](#ckeditor5/latest/getting-started/setup/editor-types.html--decoupled-editor-document) setup. By using the plugin described above together with a multi-root editor interface, users can import a Word document, edit the main content as well as headers and footers independently, and then export the entire updated document back to Word. ### Known limitations * CKEditor 5 does not support lists with skipped intermediate levels. * Due to lists merging in CKEditor 5, custom starting values are sometimes discarded by the editor. * CKEditor 5 may overwrite some table borders with its built-in styles. * Document headers and footers are not supported yet. * For track changes and comments, check the [feature comparison guide](#ckeditor5/latest/features/converters/import-word/features-comparison.html--collaboration-features) for current development. * The feature does not support `.doc` files. ### Related features * The [paste from Word](#ckeditor5/latest/features/pasting/paste-from-office.html) feature allows you to paste content from Microsoft Word and keep the original structure and formatting. * The [export to Word](#ckeditor5/latest/features/converters/export-word.html) feature allows you to generate editable `.docx` files out of your editor-created content. * The [export to PDF](#ckeditor5/latest/features/converters/export-pdf.html) feature allows you to generate portable PDF files out of your editor-created content. ### Common API The [ImportWord](../../../api/module%5Fimport-word%5Fimportword-ImportWord.html) plugin registers: * The `'importWord'` UI button component that opens the native file browser to let you import a Word file directly from your disk. * The `'importWord'` command implemented by the [ImportWordCommand](../../../api/module%5Fimport-word%5Fimportwordcommand-ImportWordCommand.html) that accepts the file to import. #### The `dataInsert` event The [dataInsert](../../../api/module%5Fimport-word%5Fimportwordcommand-ImportWordCommand.html#event-dataInsert) event is fired by [ImportWordCommand](../../../api/module%5Fimport-word%5Fimportwordcommand-ImportWordCommand.html). It allows for modifying the HTML content before inserting it into the editor. ``` editor.commands.get( 'importWord' ).on( 'dataInsert', ( event, data ) => { // The `data.html` property contains the HTML returned by the converter service. // Updating its value modifies the content that will be inserted into the editor. data.html = '

An example paragraph.

'; } ); ``` Also, you can prevent the event from further processing, and stop the import, by calling the [event.stop()](../../../api/module%5Futils%5Feventinfo-EventInfo.html#member-stop) function. ``` editor.commands.get( 'importWord' ).on( 'dataInsert', ( event, data ) => { // Example: Do not insert the HTML if it contains a table. if ( data.html.includes( ' ### REST API The DOCX to HTML converter provides an API for converting Microsoft Word `.docx` and `.dotx` files to HTML. Read the [REST API documentation](https://docx-converter.cke-cs.com/v2/convert/docs#section/Import-from-Word) to find out how to employ it in your implementation.
### Collaboration features integration Import from Word automatically integrates with the [comments](#ckeditor5/latest/features/collaboration/comments/comments.html) and [track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) features if these features are enabled and configured in your editor setup. When collaboration features are enabled, the `data` parameter in the `dataInsert` event may include the `comment_threads` and `suggestions` fields, which will hold data from the imported Word file. source file: "ckeditor5/latest/features/custom-components.html" ## Custom widgets and components CKEditor 5’s widget system allows developers to create custom interactive components that integrate with the editor’s content model. Widgets provide a structured way to embed complex content blocks, inline elements, data-driven components, and framework integrations within the editor. The examples below demonstrate the functionality and implementation approaches for each widget type. ### Block widgets: self-contained content components Developers can build block widgets that create structured content blocks functioning as independent units within documents. The demo below shows a created from scratch simple block widget with title and description slots. You can [learn how to build block widgets](#ckeditor5/latest/framework/tutorials/widgets/implementing-a-block-widget.html) in our framework section. **Example components developers can build:** * Product displays with images, pricing, and purchase options, * Team member profiles with photos and contact information, * Content cards for articles, case studies, and news items, * Data visualization components and interactive charts, * Alert boxes, callouts, and feature highlights. ### Inline widgets: dynamic elements within text Developers can create inline widgets as interactive elements that integrate seamlessly within text content without disrupting document flow. The demo below shows a simple and custom build placeholder feature. We have more advanced version of this feature, [Merge fields](#ckeditor5/latest/features/merge-fields.html), but this serves as a good example of what’s possible. You can [learn how to build inline widgets](#ckeditor5/latest/framework/tutorials/widgets/implementing-an-inline-widget.html) in our framework section. **Example components developers can build:** * Dynamic data displays for pricing, stock information, or weather, * User mentions and employee directory references, * Status indicators for project phases and workflows, * Badge elements for ratings, certifications, and labels, ### External data widgets: live updating components Developers can build widgets that connect to external APIs and data sources to display real-time information directly within editor content. The editor below contains a widget that fetches data from an external source and updates all its instances in a set interval of time. In this particular example, the widget shows the current Bitcoin rate. You can [learn how to build widgets with external data](#ckeditor5/latest/framework/tutorials/widgets/data-from-external-source.html) in our framework section. **Example components developers can build:** * Financial data including stock prices and market indicators, * Business metrics and KPI dashboards, * Live feeds from social media, news sources, and events, * Inventory systems with product availability and pricing, * API integrations for CRM data and system notifications, * Analytics displays with traffic and conversion metrics. ### React (and other frameworks) components in widgets: modern UI integrations Developers can integrate components from popular UI frameworks like React, Vue, Angular, and others into CKEditor 5, enabling reuse of existing component libraries and business logic. The editor below presents integration between React library and a block widget from the CKEditor ecosystem. You can [learn how to build widgets with React](#ckeditor5/latest/framework/tutorials/widgets/using-react-in-a-widget.html) in our framework section. **Example applications developers can build:** * Design system component integration across frameworks, * Complex multi-step forms and configuration panels that streamline content creation workflows. While CKEditor 5 provides an example React integration, similar patterns can be applied to other frameworks when building custom widgets. ### More complex features: components and editor’s UI Developers can build sophisticated features that go beyond simple content widgets to include rich interactive UI elements like balloons, dropdowns, and contextual panels. These features combine content modeling, conversion pipelines, commands, and custom UI components to create seamless editing experiences. The demo below shows an abbreviation feature that presents a balloon panel for user input when adding abbreviations to text. You can [learn how to build features with balloon UI](#ckeditor5/latest/framework/tutorials/abbreviation-plugin/abbreviation-plugin-level-1.html) in our framework section. ### Architecture overview CKEditor 5’s architecture provides a comprehensive framework for building complex features that integrate content handling with sophisticated user interfaces: **[Schema system](#ckeditor5/latest/framework/architecture/editing-engine.html--schema)** – Defines content structure, validation rules, and element relationships to ensure data integrity and feature compatibility. **[Conversion pipeline](#ckeditor5/latest/framework/architecture/editing-engine.html--conversion)** – Transforms data between the model, editing view, and data view, enabling seamless integration with external formats and real-time collaboration. **[Command architecture](#ckeditor5/latest/framework/architecture/core-editor-architecture.html--commands)** – Implements user actions, business logic, and state management with built-in undo/redo support and collaboration-ready operation handling. **[UI integration](#ckeditor5/latest/framework/architecture/ui-library.html)** – Supports toolbar buttons, [contextual balloons](#ckeditor5/latest/framework/architecture/ui-library.html--view-collections-and-the-ui-tree), dropdowns, and custom panel elements with automatic [focus management](#ckeditor5/latest/framework/deep-dive/ui/focus-tracking.html) and accessibility features. **[Event system](#ckeditor5/latest/framework/deep-dive/event-system.html)** – Provides a robust foundation for inter-component communication, user interaction handling, and plugin coordination through observable patterns. **[Plugin architecture](#ckeditor5/latest/framework/architecture/core-editor-architecture.html--plugins)** – Enables modular feature development with dependency management, lifecycle hooks, and seamless integration with existing editor functionality. source file: "ckeditor5/latest/features/document-outline.html" ## Document outline The document outline feature displays the list of sections (headings) of the document next to the editor. The outline updates automatically as the user works on the document. It offers quick navigation to a specific section upon clicking. ### Demo When the feature is enabled and configured, the outline can be displayed next to the document as presented below. The placement of the outline is [configurable](#ckeditor5/latest/features/document-outline.html--configuration) and depends on the HTML structure of the integration. The demo below showcases one of the recommended integrations but more are possible. See the [demo code](#ckeditor5/latest/features/document-outline.html--demo-code) to learn more. This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. #### Demo code For the best user experience the document outline feature requires some effort from integrators. Because it renders independently from the rest of the editor user interface (toolbar, edited content), its placement, dimensions, and behavior (like toggling) depend on the layout and functionality of the web page. The presented demo is a custom UI built on top of the [DecoupledEditor](../api/module%5Feditor-decoupled%5Fdecouplededitor-DecoupledEditor.html) (similar to the [document editor](#ckeditor5/latest/framework/deep-dive/ui/document-editor.html)). The editor in this demo loads: * A set of common editor plugins ([Essentials](../api/module%5Fessentials%5Fessentials-Essentials.html), [Bold](../api/module%5Fbasic-styles%5Fbold-Bold.html), [Heading](../api/module%5Fheading%5Fheading-Heading.html), etc.). * The [DocumentOutline](../api/module%5Fdocument-outline%5Fdocumentoutline-DocumentOutline.html) plugin. * A custom `DocumentOutlineToggler` plugin created for this particular demo to allow toggling the visibility of the outline (learn more in the [step-by-step tutorial](#ckeditor5/latest/framework/tutorials/crash-course/editor.html)). You can find the entire integration code below. View the editor configuration script View the page layout and styles ```

Initial content of the editor

```
### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: #### Activating the feature To use this premium feature, you need to activate it with proper credentials. Refer to the [License key and activation](#ckeditor5/latest/getting-started/licensing/license-key-and-activation.html) guide for details. ### Configuration #### Configuring the container The container element is essential for the document outline to render. You should pass the reference to the container element in the [config.documentOutline.container](../api/module%5Fdocument-outline%5Fdocumentoutline-DocumentOutlineConfig.html#member-container) configuration option. ``` documentOutline: { // Make sure the .document-outline-container element exists when the editor is being created. container: document.querySelector( '.document-outline-container' ) } ``` #### Customizing the look The look of the document outline can be customized using CSS classes and custom properties. In the demo below, the following customizations were applied: * The indentation of outline items was reduced (custom properties: `--ck-document-outline-indent-level-[2-3]`). * The active item color was changed (`--ck-document-outline-item-active-color` custom property). * The font size and line height were reduced for a more compact look (`.ck-document-outline__item` CSS class). * Different bullets were added for each level for better readability (`.ck-document-outline__item_level-[2-3]` CSS classes). View the document outline customization code ``` .customization-demo { --ck-document-outline-indent-level-2: 1.1em; --ck-document-outline-indent-level-3: 2.2em; --ck-document-outline-item-active-color: hsl(340deg 82% 52%); } .customization-demo .ck-document-outline__item { line-height: 1.1em; } .customization-demo .ck-document-outline__item::before { margin: 0 .2em 0 0; } .customization-demo .ck-document-outline__item.ck-document-outline__item_level-2::before, .customization-demo .ck-document-outline__item.ck-document-outline__item_level-5::before { content: "•"; } .customization-demo .ck-document-outline__item.ck-document-outline__item_level-3 { font-size: .9em; } .customization-demo .ck-document-outline__item.ck-document-outline__item_level-3::before, .customization-demo .ck-document-outline__item.ck-document-outline__item_level-6::before { content: "‣"; } .customization-demo .ck-document-outline__item.ck-document-outline__item_level-4, .customization-demo .ck-document-outline__item.ck-document-outline__item_level-5, .customization-demo .ck-document-outline__item.ck-document-outline__item_level-6 { font-size: .8em; } .customization-demo .ck-document-outline__item.ck-document-outline__item_level-4::before { content: "⁃"; } ``` ### Related features Here are some more CKEditor 5 features that can help you navigate the content of the editor: * [Table of contents](#ckeditor5/latest/features/table-of-contents.html) – Insert a table of contents widget into the document. * [Content minimap](#ckeditor5/latest/features/minimap.html) – Navigate the document using a miniature overview map placed next to the editor. * [Pagination](#ckeditor5/latest/features/pagination/pagination.html) – See the live preview of the document’s page breaks and quickly navigate between pages. source file: "ckeditor5/latest/features/drag-drop.html" ## Drag and drop The drag and drop feature lets you drag and drop both text and content blocks such as paragraphs, tables, or lists inside the editor. This allows you to select an entire block or multiple blocks, and move them before or after other blocks. You can also drag and drop HTML and plain text content from outside the editor and use it to upload images. ### Demo The demo below lets you drag contacts from the list to the editor. The contacts are inserted into the editor as custom widgets representing the [h-card microformat](http://microformats.org/wiki/h-card). You can also select and drag around existing content inside the editor. Photos: [Wikipedia.org](http://en.wikipedia.org). This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. The source code of the above snippet is available here: [drag-drop.js](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-clipboard/docs/%5Fsnippets/features/drag-drop.js), [drag-drop.html](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-clipboard/docs/%5Fsnippets/features/drag-drop.html). You can find the configuration of the editor used in the demo here: [build-drag-drop-source.js](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-clipboard/docs/%5Fsnippets/features/build-drag-drop-source.js). The code for the custom plugin responsible for handling the h-cards is available here: [hcard.js](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-clipboard/docs/%5Fsnippets/features/hcard.js). ### File upload via drag and drop When the [CKBox file manager](#ckeditor5/latest/features/file-management/ckbox.html) is enabled in your CKEditor 5 integration, you can upload files and images using the drag and drop mechanism. You can test this solution in the [CKBox demo](#ckeditor5/latest/features/file-management/ckbox.html--demo). ### Drag and drop of content blocks The drag and drop plugin fully supports dragging content blocks such as paragraphs, tables, or lists inside the editor by default. This allows you to select an entire block or multiple blocks, and move them before or after other blocks. The drag and drop functions include: * Selection of the text, elements, multiple blocks, and moving these around. * Placement of blocks inside other blocks such as tables, blockquotes, etc. * The braille dots panel icon in the [balloon block editor](#ckeditor5/latest/features/drag-drop.html--balloon-block-editor-demo) now behaves as a drag handle. #### Classic editor demo Select a block or blocks, and drag them across the document. You can place blocks inside other blocks, such as tables and blockquotes. #### Balloon block editor demo In the balloon block editor, you can also drag content blocks using the drag handle. Select or focus on the block, and then drag the block with the braille dots panel icon . ### Installation After [installing the editor](#ckeditor5/latest/getting-started/installation/cloud/quick-start.html), add the feature to your plugin list and toolbar configuration: The [DragDrop](../api/module%5Fclipboard%5Fdragdrop-DragDrop.html) plugin will activate along with the clipboard plugin. ### Styling the drag and drop The drag and drop target line color is managed by the CSS variable (`--ck-clipboard-drop-target-color`). You can use the following snippet to change the color of the line: ``` :root { --ck-clipboard-drop-target-color: green; } ``` ### Related features * CKEditor 5 supports dropping images from the file system thanks to the [image upload](#ckeditor5/latest/features/images/image-upload/image-upload.html) feature. ### Contribute The source code of the feature is available on GitHub at . source file: "ckeditor5/latest/features/editor-placeholder.html" ## Editor placeholder You can prompt the user to input content by displaying a configurable placeholder text when the editor is empty. This works similarly to the native DOM [placeholder attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-placeholder) used by inputs. Not to be confused with the content placeholders offered by the [merge fields](#ckeditor5/latest/features/merge-fields.html) feature. ### Demo See the demo of the placeholder feature: This demo presents a limited set of features. Visit the [feature-rich editor example](#ckeditor5/latest/examples/builds-custom/full-featured-editor.html) to see more in action. ### Installation The editor placeholder feature does not require a separate plugin installation. It does, however, require configuring the editor before use. There are two different ways of configuring the editor placeholder text: #### Using the `placeholder` attribute of a textarea Set the `placeholder` attribute on a ` ``` #### Using the editor configuration You can use the [editor.config.root.placeholder](../api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-root) configuration option: * when no element was passed into `Editor.create()` method, * when the element passed into `Editor.create()` was not a `

``` Classic editor will automatically update the value of the ` ``` Thanks to that, the ` ``` Instead of being printed like this: ``` ``` While simple content like that mentioned above does not itself require to be encoded, encoding the data will prevent losing text like `<` or ``.
### Updating the source element If the source element is not ` ``` #### Step 2: Replace CKEditor 5 imports with `window.CKEDITOR` Since the CKEditor 5 script is now included via the CDN, you can access the `ClassicEditor` object directly in your JavaScript file using the `window.CKEDITOR` global variable. It means that `import` statements are no longer needed and you can remove them from your JavaScript files. Here is an example of migrating the CKEditor 5 initialization code: **Before:** ``` import { ClassicEditor } from 'ckeditor5'; import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; ClassicEditor .create( { attachTo: document.querySelector('#editor'), licenseKey: '', // Or 'GPL'. // ... other configuration } ) .catch( error => { console.error(error); } ); ``` **After:** ``` const { ClassicEditor } = window.CKEDITOR; const { AIAdapter, /* ... other imports */ } = window.CKEDITOR_PREMIUM_FEATURES; ClassicEditor .create( { attachTo: document.querySelector('#editor'), licenseKey: '', // ... other configuration } ) .catch( error => { console.error(error); } ); ``` ### Using lazy injection of CKEditor 5 If you prefer to automatically inject the CKEditor 5 script into your HTML file, you can migrate your project using the `@ckeditor/ckeditor5-integrations-common` package. This package provides a `loadCKEditorCloud` function that automatically injects the CKEditor 5 scripts and styles into your HTML file. It may be useful when your project uses a bundler like Webpack or Rollup and you cannot modify your head section directly. #### Step 1: Install the `@ckeditor/ckeditor5-integrations-common` Package First, install the `@ckeditor/ckeditor5-integrations-common` package using the following command: ``` npm install @ckeditor/ckeditor5-integrations-common ``` #### Step 2: Replace CKEditor 5 Imports If you have any CKEditor 5 imports in your JavaScript files, remove them. For example, remove lines like: ``` import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; ``` Next, update your JavaScript file to use the `loadCKEditorCloud` function from the `@ckeditor/ckeditor5-integrations-common` package. Here is an example of migrating the CKEditor 5 initialization code: **Before:** ``` import { ClassicEditor } from 'ckeditor5'; ClassicEditor .create( { attachTo: document.querySelector('#editor') } ) .catch( error => { console.error(error); } ); ``` **After:** ``` import { loadCKEditorCloud } from '@ckeditor/ckeditor5-integrations-common'; const { ClassicEditor } = await loadCKEditorCloud( { version: '48.0.0', } ); ``` ### Migrating custom plugins If you are using custom plugins, you need to first [adapt these to work with the CDN approach](#ckeditor5/latest/framework/tutorials/creating-simple-plugin-timestamp.html--adapt-this-tutorial-to-cdn). It will change the way imports and CSS files are handled. Next, refer to the [Loading CDN resources](#ckeditor5/latest/getting-started/setup/loading-cdn-resources.html) guide to learn about using the [loadCKEditorCloud](#ckeditor5/latest/getting-started/setup/loading-cdn-resources.html--using-the-useckeditorcloud-function) function. This will be needed to include the custom plugin in the CDN configuration of CKEditor 5\. By employing these, you can dynamically import the plugin in a way similar to this one: ``` plugins: { CustomPlugin: () => import('./path/to/plugin.umd.js') } ``` If your plugin depends on core plugins already loaded via CDN, you can access them through the `window.CKEDITOR` and `window.CKEDITOR_PREMIUM_FEATURES` global variables. Also, `loadCKEditorCloud` uses caching, so you can call it to dynamically load dependencies. Find more details in the options section of the [Loading CDN resources](#ckeditor5/latest/getting-started/setup/loading-cdn-resources.html--the-loadckeditorcloud-function-options) guide. ### Conclusion Following these steps, you successfully migrated CKEditor 5 from an NPM-based installation to a CDN-based installation using Vanilla JS. This approach simplifies the setup process and can help improve the performance of your application by reducing the bundle size. source file: "ckeditor5/latest/updating/migrations/vuejs-v3.html" ## Migrating Vue.js 3+ CKEditor 5 integration from npm to CDN This guide will help you migrate Vue 3 CKEditor 5 integration from an NPM-based installation to a CDN-based one. ### Prerequisites Remove the existing CKEditor 5 packages from your project. If you are using the NPM-based installation, you can remove it by running the following command: ``` npm uninstall ckeditor5 ckeditor5-premium-features ``` Upgrade the CKEditor 5 Vue 3 integration to the latest version. You can find the latest version in the [Vue 3 integration](#ckeditor5/latest/getting-started/installation/cloud/vuejs-v3.html) documentation. Ensure that your testing suite uses real web browser environments for testing. If you are using `jsdom` or any other environment without a real DOM, you may need to adjust the testing suite configuration to use a real browser because CDN script injection might not be recognized properly in such environments. ### Migration steps #### Step 1: Remove CKEditor 5 imports If you have any CKEditor 5 imports in your Vue components, remove them. For example, remove lines like: ``` import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; ``` #### Step 2: Update your Vue components to use CDN Replace the CKEditor 5 NPM package imports with the CDN script imports and use the `useCKEditorCloud` function to load the CKEditor 5 scripts. The `useCKEditorCloud` function is a part of the `@ckeditor/ckeditor5-vue` package and is used to load CKEditor 5 scripts from the CKEditor Cloud service. **Before:** ``` ``` **After:** ``` ``` #### Step 3 (Optional): Migrate the CKEditor 5 Vue 3+ integration testing suite If you have any tests that use CKEditor 5 objects, you need to update them to use the `loadCKEditorCloud` function. Here is an example of migrating a test that uses the `ClassicEditor` object: **Before:** ``` import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; it( 'ClassicEditor test', () => { // Your test that uses the CKEditor 5 object. } ); ``` **After:** ``` // It may be counterintuitive that in tests you need to use `loadCKEditorCloud` instead of `useCKEditorCloud`. // The reason for this is that `useCKEditorCloud` is composable and can only be used in Vue components, // while tests are typically written as functions in testing suites. Therefore, in tests, you should use // the `loadCKEditorCloud` function to load CKEditor 5 from the CKEditor Cloud and obtain the necessary // CKEditor 5 objects. This allows you to properly test your CKEditor 5 integration without any issues. import { loadCKEditorCloud } from '@ckeditor/ckeditor5-vue'; let cloud; beforeEach( async () => { cloud = await loadCKEditorCloud( { version: '48.0.0', } ); } ); it( 'ClassicEditor test', () => { const { ClassicEditor, ... } = cloud.CKEditor; // Your test that uses the CKEditor 5 object. } ); ``` #### Step 4 (Optional): Clean up the document head entries before each test The `useCKEditorCloud` composable under the hood injects the CKEditor 5 scripts and styles into your document head. If you use a testing suite that does not Clean up the document head entries before each test, you may need to do it manually. This is important because the `useCKEditorCloud` composable might reuse the same head entries for each test, which can lead to skipping the `loading` state and directly going to the `success` state. It may cause some tests that rely on the `loading` state to fail. However, there is one downside to this approach. Cleaning up the head entries before each test may slow down the test execution because the browser needs to download the CKEditor 5 script each time. In most cases, this should not be a problem, but if you notice that your tests are running slower, you may need to consider other solutions. Here is an example of how you can Clean up the document head entries before each test: ``` import { removeAllCkCdnResources } from '@ckeditor/ckeditor5-integrations-common/test-utils'; beforeEach( () => { removeAllCkCdnResources(); } ); ``` The code above will remove all CKEditor 5 CDN scripts, style sheets, and Window objects from the head section of your HTML file before each test, making sure that the `useCKEditorCloud` composable will inject the CKEditor 5 scripts and styles again. source file: "ckeditor5/latest/updating/nim-migration/custom-plugins.html" ## Migrating custom plugins If you have created and published custom plugins for CKEditor 5, you will need to adjust them to make them work with the new installation methods. You do not need to follow this guide if your custom plugins are used directly in your project and are not separate packages. In such cases, you can update the plugins along with the projects that use them. ### Prerequisites Before you start, follow the usual upgrade path to update your plugin to use the latest version of CKEditor 5\. This will rule out any problems that may be caused by upgrading from an outdated version of CKEditor 5. ### Migration steps #### Create a new project using the package generator To ensure that all the dependencies are up-to-date and that the build process is correct, we recommend the following steps: 1. Create a new project using the package generator following the [package generator guide](#ckeditor5/latest/framework/develpment-tools/package-generator/using-package-generator.html). 2. Copy the `src`, `tests`, and `sample` folders of your plugin into the new project. 3. Re-add all the external `dependencies`, `devDependencies`, and `peerDependencies` specific to your plugin to the `package.json` file. When you run the CLI, it generates the modern build setup and the imports used across the new ecosystem. The main changes we have introduced in the new package generator are: * Making the generated package a valid ECMAScript module, * Updating the build process to generate bundles for the new installation methods, * Adding new eslint rules to avoid common errors, * Updating dependencies. #### Add missing file extensions in imports Next, as required by the JavaScript modules (ESM), you must add the missing file extensions to all files in the `src`, `tests`, and `sample` folders during import. ``` - import { Plugin } from 'ckeditor5/src/core'; + import { Plugin } from 'ckeditor5'; -import SomePlugin from './src/someplugin'; +import SomePlugin from './src/someplugin.js'; ``` Imports from the package roots should be changed to `ckeditor5`. ``` - import { Plugin } from '@ckeditor/ckeditor5-core'; + import { Plugin } from 'ckeditor5'; ``` If you run the following command, the `ckeditor5-rules/require-file-extensions-in-imports` eslint rule should fix most, if not all, problems related to missing file extensions. ``` npm run lint -- --fix ``` #### Remove `src` folders from the import paths For some time now, we have strongly discouraged importing from the `src` folder of the `@ckeditor/ckeditor5-*` packages. Instead, you should import from the package roots because they provide better TypeScript support and because the `src` folders are now removed. Always import from the `ckeditor5`. ``` // ❌ import Plugin from '@ckeditor/ckeditor5-core/src/plugin.js'; // ❌ import { Plugin } from '@ckeditor/ckeditor5-core'; // ✅ import { Plugin } from 'ckeditor5'; ``` Note that the names of the exports may differ between the `src` folder and the package root. In the above example, the named `Plugin` import from `@ckeditor/ckeditor5-core/src/plugin.js` will be exported under the same name from `ckeditor5`, but this is not guaranteed. In cases where the names do not match, you will need to modify the import accordingly. There may also be cases where something you imported from the `src` folder is not exported from the package root. In such cases, please create a new issue in the [CKEditor 5 repository](https://github.com/ckeditor/ckeditor5/issues/new/choose) so we can consider adding the missing exports. If you run the following command, the `ckeditor5-rules/allow-imports-only-from-main-package-entry-point` eslint rule will list all the places where you need to update the imports. ``` npm run lint ``` #### Remove `theme` folders from the import paths The same rule applies to the `theme` folder in the `@ckeditor/ckeditor5-*` packages. If you need to use icons from this folder, you can likely import them from the package root. ``` // ❌ import undo from '@ckeditor/ckeditor5-icons/theme/undo.svg'; console.log( undo ); // ✅ import { IconUndo } from 'ckeditor5'; console.log( IconUndo ); ``` If you run the following command, the `ckeditor5-rules/allow-imports-only-from-main-package-entry-point` eslint rule will list all the places where you need to update the imports. ``` npm run lint ``` #### Update imports to the `ckeditor5` package Update all imports from `ckeditor5/src/*` and `@ckeditor/ckeditor5-*` to `ckeditor5`. ``` - import { Plugin } from 'ckeditor5/src/core.js'; - import { ButtonView } from 'ckeditor5/src/ui.js'; + import { Plugin, ButtonView } from 'ckeditor5'; ``` If you run the following command, the `ckeditor5-rules/no-legacy-imports` eslint rule will list all the places where you need to update the imports. ``` npm run lint ``` #### Run eslint Run the `npm run lint` command to see if there are any remaining problems that need to be fixed. ### Generate and validate the bundle Once you have updated all the imports, it is time to build and validate the bundle for the new installation methods. 1. Build the plugin with the following command. It will create the `dist` folder with the plugin bundles for the new installation methods. ``` npm run build ``` 2. Inspect the imports at the top of the `dist/index.js` file. You should only see imports from `ckeditor5` (not from `ckeditor5/src/*`) and optionally from other external dependencies. 3. Repeat the above step for the `dist/browser/index.es.js` file, but this time you should only see imports from `ckeditor5` or `ckeditor5-premium-features`. All other imports, including external dependencies, should be bundled with the plugin. If you see imports in the second or third step that are not explicitly mentioned, check where the imports come from in the source code and if they have been updated according to the above migration steps. If this is the case and the imports in the generated bundle are still incorrect, please create a new issue in the [CKEditor 5 repository](https://github.com/ckeditor/ckeditor5/issues/new/choose). ### How to use your plugin in new installation methods? Once the package is migrated, follow the [Build output and integration](#ckeditor5/latest/framework/develpment-tools/package-generator/build-output-and-integration.html) guide to integrate it with npm, ZIP, or CDN setups. source file: "ckeditor5/latest/updating/nim-migration/customized-builds.html" ## Migrating from customized builds Migrating from a customized build to the new installation methods should mostly be a matter of changing the way you import CKEditor 5 and its plugins. Regardless of whether you used our old Online Builder or created a custom build from source using webpack or Vite, the new installation methods allow you to build and run the editor with any bundler or JavaScript meta-framework you like. This means that by the end of the migration, you can remove the CKEditor-specific webpack or Vite setup from your project if you already use another bundler to build your project. ### Prerequisites Before you start, follow the usual upgrade path to update your project to use the latest version of CKEditor 5\. This will rule out any problems that may be caused by upgrading from an outdated version of the editor. ### Migration steps If you are using the customized build, follow the steps below: 1. Start by uninstalling all CKEditor 5 packages that you have installed in your project. This includes the main `ckeditor5` package and any additional plugins that you have installed separately. ``` npm uninstall \ @ckeditor/ckeditor5-adapter-ckfinder \ @ckeditor/ckeditor5-alignment \ @ckeditor/ckeditor5-autoformat \ @ckeditor/ckeditor5-autosave \ @ckeditor/ckeditor5-basic-styles \ @ckeditor/ckeditor5-block-quote \ @ckeditor/ckeditor5-ckbox \ @ckeditor/ckeditor5-ckfinder \ @ckeditor/ckeditor5-clipboard \ @ckeditor/ckeditor5-cloud-services \ @ckeditor/ckeditor5-code-block \ @ckeditor/ckeditor5-core \ @ckeditor/ckeditor5-easy-image \ @ckeditor/ckeditor5-editor-balloon \ @ckeditor/ckeditor5-editor-classic \ @ckeditor/ckeditor5-editor-decoupled \ @ckeditor/ckeditor5-editor-inline \ @ckeditor/ckeditor5-editor-multi-root \ @ckeditor/ckeditor5-engine \ @ckeditor/ckeditor5-enter \ @ckeditor/ckeditor5-essentials \ @ckeditor/ckeditor5-find-and-replace \ @ckeditor/ckeditor5-font \ @ckeditor/ckeditor5-heading \ @ckeditor/ckeditor5-highlight \ @ckeditor/ckeditor5-horizontal-line \ @ckeditor/ckeditor5-html-embed \ @ckeditor/ckeditor5-html-support \ @ckeditor/ckeditor5-image \ @ckeditor/ckeditor5-indent \ @ckeditor/ckeditor5-language \ @ckeditor/ckeditor5-link \ @ckeditor/ckeditor5-list \ @ckeditor/ckeditor5-markdown-gfm \ @ckeditor/ckeditor5-media-embed \ @ckeditor/ckeditor5-mention \ @ckeditor/ckeditor5-minimap \ @ckeditor/ckeditor5-page-break \ @ckeditor/ckeditor5-paragraph \ @ckeditor/ckeditor5-paste-from-office \ @ckeditor/ckeditor5-remove-format \ @ckeditor/ckeditor5-restricted-editing \ @ckeditor/ckeditor5-select-all \ @ckeditor/ckeditor5-show-blocks \ @ckeditor/ckeditor5-source-editing \ @ckeditor/ckeditor5-special-characters \ @ckeditor/ckeditor5-style \ @ckeditor/ckeditor5-table \ @ckeditor/ckeditor5-theme-lark \ @ckeditor/ckeditor5-typing \ @ckeditor/ckeditor5-ui \ @ckeditor/ckeditor5-undo \ @ckeditor/ckeditor5-upload \ @ckeditor/ckeditor5-utils \ @ckeditor/ckeditor5-watchdog \ @ckeditor/ckeditor5-widget \ @ckeditor/ckeditor5-word-count \ @ckeditor/ckeditor5-ai \ @ckeditor/ckeditor5-case-change \ @ckeditor/ckeditor5-collaboration-core \ @ckeditor/ckeditor5-comments \ @ckeditor/ckeditor5-document-outline \ @ckeditor/ckeditor5-export-pdf \ @ckeditor/ckeditor5-export-word \ @ckeditor/ckeditor5-format-painter \ @ckeditor/ckeditor5-import-word \ @ckeditor/ckeditor5-list-multi-level \ @ckeditor/ckeditor5-pagination \ @ckeditor/ckeditor5-paste-from-office-enhanced \ @ckeditor/ckeditor5-real-time-collaboration \ @ckeditor/ckeditor5-revision-history \ @ckeditor/ckeditor5-slash-command \ @ckeditor/ckeditor5-template \ @ckeditor/ckeditor5-track-changes \ ckeditor5 \ ckeditor5-collaboration ``` 2. Next, install the `ckeditor5` package. This package contains the editor and all of our open-source plugins. ``` npm install ckeditor5 ``` 3. (Optional) If you are using premium features from our commercial offer, you should also install the `ckeditor5-premium-features` package. ``` npm install ckeditor5-premium-features ``` 4. Open the file where you initialized the editor. Then, replace the import statements to import the editor and all the open-source plugins from the `ckeditor5` package and the commercial plugins from the `ckeditor5-premium-features` package only. ``` import { ClassicEditor, Essentials, Bold, Italic, Paragrap, Mention } from 'ckeditor5'; import { FormatPainter, SlashCommand } from 'ckeditor5-premium-features'; ``` 5. Below these imports, add imports of the CSS styles for the editor and the commercial plugins. ``` import 'ckeditor5/ckeditor5.css'; import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; ``` ### Example Below is a comparison of the editor configuration before and after the migration. Before ``` import { ClassicEditor as ClassicEditorBase } from '@ckeditor/ckeditor5-editor-classic'; import { Essentials } from '@ckeditor/ckeditor5-essentials'; import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder'; import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; import { CKBox } from '@ckeditor/ckeditor5-ckbox'; import { CKFinder } from '@ckeditor/ckeditor5-ckfinder'; import { EasyImage } from '@ckeditor/ckeditor5-easy-image'; import { Heading } from '@ckeditor/ckeditor5-heading'; import { Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image'; import { Indent } from '@ckeditor/ckeditor5-indent'; import { Link } from '@ckeditor/ckeditor5-link'; import { List } from '@ckeditor/ckeditor5-list'; import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office'; import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; import { TextTransformation } from '@ckeditor/ckeditor5-typing'; import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; export default class ClassicEditor extends ClassicEditorBase { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, EasyImage, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } ``` After ``` import { ClassicEditor as ClassicEditorBase, Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; export default class ClassicEditor extends ClassicEditorBase { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } ``` source file: "ckeditor5/latest/updating/nim-migration/dll-builds.html" ## Migrating from DLL builds DLLs are webpack-specific builds that register CKEditor 5 and its plugins in a globally scoped variable `CKEditor5`. This variable could then be used to create the editor instance. Since the new installation methods do not rely on global variables, migrating from the DLL builds to the new installation methods should mostly be a matter of changing the way you import CKEditor 5 and its plugins. Other notable difference is that DLLs use the ` ``` 2.2 If you also use premium features from our commercial offer: ``` ``` 3. Replace the old ` ``` 3.2 If you also use premium features from our commercial offer: ``` ``` ### Example Below is the comparison of the editor configuration before and after the migration. Before ``` ``` After ``` ``` source file: "ckeditor5/latest/updating/nim-migration/migrating-imports.html" ## Migrating imports (v46+) As part of the transition to the New Installation Methods (NIM) in version, we have standardized how public API elements are exposed in CKEditor 5 and related packages. We introduced a unified export policy that ensures every public entity is exported via the package’s `index.ts` file. We also gave the exported classes, functions, and helpers more descriptive and context-appropriate names ensuring they are unambiguous and unique within the scope of CKEditor 5\. This includes renaming existing exports where needed. The changes are semantically equivalent but introduce breaking changes in naming. ### Using internal APIs For a long time in Old Installation Methods (OIM), it was possible to grab internal-purpose functions from specific files if they were exported just for the package’s internal purposes. We want to clean up this situation in the New Installation Methods, and internal APIs will not be available in the index. After deprecating OIM, source files will not be available, so some internal APIs that were previously used in this way will no longer be available. To ease the migration, all internal exports that were available before, are now available directly from the `ckeditor5` (or `ckeditor5-premium-features`) package root with an underscore (`_`) prefix. Keep in mind that: * we may remove them in the future. * all new internal methods will not be added to the indexes. If you think one of our internal methods, classes, etc. should be available in the public API, please let us know via [the support channel](https://ckeditor.com/contact/) or on [the GitHub issue tracker](https://github.com/ckeditor/ckeditor5/issues). #### Example migration Previously, importing internal exports required specifying the exact file path: ``` import { getCsrfToken } from '@ckeditor/ckeditor5-adapter-ckfinder/src/utils'; ``` Now, you can import them from the package’s root entry point with the new name: ``` import { _getCKFinderCsrfToken } from 'ckeditor5'; ``` ### Changed exports Below, you will find all name changes in packages listed alphabetically for convenience. #### @ckeditor/ckeditor5-adapter-ckfinder | file | original name | re-exported name | | -------- | ------------- | ---------------------- | | utils.ts | getCsrfToken | \_getCKFinderCsrfToken | | utils.ts | getCookie | \_getCKFinderCookie | | utils.ts | setCookie | \_setCKFinderCookie | #### @ckeditor/ckeditor5-ai | file | original name | re-exported name | | ---------------------------- | ---------------------- | ---------------------- | | adapters/aitextadapter.ts | RequestHeaders | AIRequestHeaders | | adapters/aitextadapter.ts | RequestParameters | AIRequestParameters | | adapters/awstextadapter.ts | AWSTextAdapterConfig | AIAWSTextAdapterConfig | | adapters/awstextadapter.ts | AWSModelFamily | AIAWSModelFamily | | aiasistant.ts | getDefaultCommands | getDefaultAICommands | | aiasistant.ts | GroupDefinition | AIGroupDefinition | | aiasistant.ts | CommandDefinition | AICommandDefinition | | aiconfig.ts | AIConfig | AIConfig | | ui/showaiassistantcommand.ts | ShowAIAssistantCommand | ShowAIAssistantCommand | #### @ckeditor/ckeditor5-alignment | file | original name | re-exported name | | ------------------ | ------------------------- | ------------------------------- | | alignmentconfig.ts | SupportedOption | AlignmentSupportedOption | | utils.ts | supportedOptions | \_ALIGNMENT\_SUPPORTED\_OPTIONS | | utils.ts | isSupported | \_isAlignmentSupported | | utils.ts | isDefault | \_isDefaultAlignment | | utils.ts | normalizeAlignmentOptions | \_normalizeAlignmentOptions | #### @ckeditor/ckeditor5-autoformat | file | original name | re-exported name | | -------------------------- | ------------- | ---------------------- | | inlineautoformatediting.ts | TestCallback | AutoformatTestCallback | #### @ckeditor/ckeditor5-basic-styles | file | original name | re-exported name | | -------- | ---------------- | ----------------------------- | | utils.ts | getButtonCreator | \_getBasicStylesButtonCreator | #### @ckeditor/ckeditor5-bookmark | file | original name | re-exported name | | -------- | ----------------- | ------------------- | | utils.ts | isBookmarkIdValid | \_isBookmarkIdValid | #### @ckeditor/ckeditor5-case-change | file | original name | re-exported name | | ------------- | --------------------------- | ------------------------------------- | | casechange.ts | ExcludeWordsCallback | CaseChangeExcludeWordsCallback | | casechange.ts | ExcludeWordsCallbackContext | CaseChangeExcludeWordsCallbackContext | #### @ckeditor/ckeditor5-ckbox | file | original name | re-exported name | | ----------------------- | ----------------------------------- | ------------------------------------- | | ckboxcommand.ts | prepareImageAssetAttributes | \_prepareCKBoxImageAssetAttributes | | ckboxconfig.ts | CKBoxAssetDefinition | \_CKBoxAssetDefinition | | ckboxconfig.ts | CKBoxAssetImageDefinition | \_CKBoxAssetImageDefinition | | ckboxconfig.ts | CKBoxAssetLinkDefinition | \_CKBoxAssetLinkDefinition | | ckboxconfig.ts | CKBoxAssetImageAttributesDefinition | \_CKBoxAssetImageAttributesDefinition | | ckboxconfig.ts | CKBoxAssetLinkAttributesDefinition | \_CKBoxAssetLinkAttributesDefinition | | ckboximageedit/utils.ts | createEditabilityChecker | \_createCKBoxEditabilityChecker | | utils.ts | getImageUrls | \_getCKBoxImageUrls | | utils.ts | getWorkspaceId | \_getCKBoxWorkspaceId | | utils.ts | blurHashToDataUrl | \_ckboxBlurHashToDataUrl | | utils.ts | sendHttpRequest | \_sendCKBoxHttpRequest | | utils.ts | convertMimeTypeToExtension | \_ckBoxConvertMimeTypeToExtension | | utils.ts | getContentTypeOfUrl | \_getCKBoxContentTypeOfUrl | | utils.ts | getFileExtension | \_getCKBoxFileExtension | #### @ckeditor/ckeditor5-clipboard | file | original name | re-exported name | | ------------------------------- | ------------------------------- | --------------------------------- | | clipboardmarkersutils.ts | ClipboardMarkersUtils | \_ClipboardMarkersUtils | | clipboardmarkersutils.ts | ClipboardMarkerRestrictedAction | \_ClipboardMarkerRestrictedAction | | clipboardmarkersutils.ts | ClipboardMarkerConfiguration | \_ClipboardMarkerConfiguration | | dragdrop.ts | DragDrop | \_DragDrop | | dragdropblocktoolbar.ts | DragDropBlockToolbar | \_DragDropBlockToolbar | | dragdroptarget.ts | DragDropTarget | \_DragDropTarget | | lineview.ts | LineView | \_ClipboardLineView | | utils/normalizeclipboarddata.ts | normalizeClipboardData | \_normalizeClipboardData | #### @ckeditor/ckeditor5-cloud-services | file | original name | re-exported name | | ----------------------------- | ------------------------------ | ------------------------------------------- | | token/token.ts | TokenOptions | CloudServicesTokenOptions | | uploadgateway/fileuploader.ts | FileUploaderErrorEvent | CloudServicesFileUploaderErrorEvent | | uploadgateway/fileuploader.ts | FileUploaderProgressErrorEvent | CloudServicesFileUploaderProgressErrorEvent | #### @ckeditor/ckeditor5-code-block | file | original name | re-exported name | | ------------- | -------------------------------------------- | ------------------------------------------------------- | | converters.ts | modelToViewCodeBlockInsertion | \_modelToViewCodeBlockInsertion | | converters.ts | modelToDataViewSoftBreakInsertion | \_modelToDataViewCodeBlockSoftBreakInsertion | | converters.ts | dataViewToModelCodeBlockInsertion | \_dataViewToModelCodeBlockInsertion | | converters.ts | dataViewToModelTextNewlinesInsertion | \_dataViewToModelCodeBlockTextNewlinesInsertion | | converters.ts | dataViewToModelOrphanNodeConsumer | \_dataViewToModelCodeBlockOrphanNodeConsumer | | utils.ts | getNormalizedAndLocalizedLanguageDefinitions | \_getNormalizedAndLocalizedCodeBlockLanguageDefinitions | | utils.ts | getPropertyAssociation | \_getCodeBlockPropertyAssociation | | utils.ts | getLeadingWhiteSpaces | \_getCodeBlockLeadingWhiteSpaces | | utils.ts | rawSnippetTextToViewDocumentFragment | \_rawCodeBlockSnippetTextToViewDocumentFragment | | utils.ts | getIndentOutdentPositions | \_getCodeBlockIndentOutdentPositions | | utils.ts | isModelSelectionInCodeBlock | \_isModelSelectionInCodeBlock | | utils.ts | canBeCodeBlock | \_canBeCodeBlock | | utils.ts | getCodeBlockAriaAnnouncement | \_getCodeBlockAriaAnnouncement | | utils.ts | getTextNodeAtLineStart | \_getCodeBlockTextNodeAtLineStart | #### @ckeditor/ckeditor5-collaboration-core | file | original name | re-exported name | | --------- | ------------- | ------------------------ | | config.ts | UsersConfig | CollaborationUsersConfig | #### @ckeditor/ckeditor5-comments | file | original name | re-exported name | | ------------------------ | --------------------------- | --------------------------- | | sidebaritemview.ts | SidebarItemView | AnnotationsSidebarItemView | | sidebarview.ts | SidebarView | AnnotationsSidebarView | | basecommentthreadview.ts | UISubmitCommentThreadEvent | UISubmitCommentThreadEvent | | basecommentthreadview.ts | UIRemoveCommentThreadEvent | UIRemoveCommentThreadEvent | | basecommentthreadview.ts | UIResolveCommentThreadEvent | UIResolveCommentThreadEvent | | basecommentview.ts | UIAddCommentEvent | UIAddCommentEvent | | basecommentview.ts | UIUpdateCommentEvent | UIUpdateCommentEvent | | basecommentview.ts | UIRemoveCommentEvent | UIRemoveCommentEvent | | config.ts | SidebarConfig | AnnotationsSidebarConfig | #### @ckeditor/ckeditor5-core | file | original name | re-exported name | | ------------------ | --------------------- | ----------------------------------- | | accessibility.ts | DEFAULT\_GROUP\_ID | \_DEFAULT\_ACCESSIBILITY\_GROUP\_ID | | accessibility.ts | KeystrokeInfos | KeystrokeInfoDefinitions | | accessibility.ts | KeystrokeInfoCategory | KeystrokeInfoCategoryDefinition | | accessibility.ts | KeystrokeInfoGroup | KeystrokeInfoGroupDefinition | | editorusagedata.ts | getEditorUsageData | \_getEditorUsageData | | editorusagedata.ts | EditorUsageData | \_EditorUsageData | #### @ckeditor/ckeditor5-emoji | file | original name | re-exported name | | ------------------- | ---------------- | ------------------ | | emojiconfig.ts | SkinToneId | EmojiSkinToneId | | emojirepository.ts | SkinTone | EmojiSkinTone | | isemojisupported.ts | isEmojiSupported | \_isEmojiSupported | #### @ckeditor/ckeditor5-engine | file | original name | re-exported name | | ------------------------------------ | ---------------------------------------- | ---------------------------------------------------------- | | conversion/downcastdispatcher.ts | DiffItemReinsert | DifferItemReinsert | | conversion/downcasthelpers.ts | insertText | \_downcastInsertText | | conversion/downcasthelpers.ts | insertAttributesAndChildren | \_downcastInsertAttributesAndChildren | | conversion/downcasthelpers.ts | remove | \_downcastRemove | | conversion/downcasthelpers.ts | createViewElementFromHighlightDescriptor | \_downcastCreateViewElementFromDowncastHighlightDescriptor | | conversion/downcasthelpers.ts | convertRangeSelection | \_downcastConvertRangeSelection | | conversion/downcasthelpers.ts | convertCollapsedSelection | \_downcastConvertCollapsedSelection | | conversion/downcasthelpers.ts | cleanSelection | \_downcastCleanSelection | | conversion/downcasthelpers.ts | wrap | \_downcastWrap | | conversion/downcasthelpers.ts | insertElement | \_downcastInsertElement | | conversion/downcasthelpers.ts | insertStructure | \_downcastInsertStructure | | conversion/downcasthelpers.ts | insertUIElement | \_downcastInsertUIElement | | conversion/downcasthelpers.ts | HighlightDescriptor | DowncastHighlightDescriptor | | conversion/downcasthelpers.ts | SlotFilter | DowncastSlotFilter | | conversion/downcasthelpers.ts | ElementCreatorFunction | DowncastElementCreatorFunction | | conversion/downcasthelpers.ts | StructureCreatorFunction | DowncastStructureCreatorFunction | | conversion/downcasthelpers.ts | AttributeElementCreatorFunction | DowncastAttributeElementCreatorFunction | | conversion/downcasthelpers.ts | AttributeCreatorFunction | DowncastAttributeCreatorFunction | | conversion/downcasthelpers.ts | AttributeDescriptor | DowncastAttributeDescriptor | | conversion/downcasthelpers.ts | MarkerElementCreatorFunction | DowncastMarkerElementCreatorFunction | | conversion/downcasthelpers.ts | HighlightDescriptorCreatorFunction | DowncastHighlightDescriptorCreatorFunction | | conversion/downcasthelpers.ts | AddHighlightCallback | DowncastAddHighlightCallback | | conversion/downcasthelpers.ts | RemoveHighlightCallback | DowncastRemoveHighlightCallback | | conversion/downcasthelpers.ts | MarkerDataCreatorFunction | DowncastMarkerDataCreatorFunction | | conversion/downcasthelpers.ts | ConsumerFunction | \_DowncastConsumerFunction | | conversion/mapper.ts | MapperCache | \_MapperCache | | conversion/upcasthelpers.ts | convertToModelFragment | \_upcastConvertToModelFragment | | conversion/upcasthelpers.ts | convertText | \_upcastConvertText | | conversion/upcasthelpers.ts | convertSelectionChange | \_upcastConvertSelectionChange | | conversion/upcasthelpers.ts | ElementCreatorFunction | UpcastElementCreatorFunction | | conversion/upcasthelpers.ts | AttributeCreatorFunction | UpcastAttributeCreatorFunction | | conversion/upcasthelpers.ts | MarkerFromElementCreatorFunction | UpcastMarkerFromElementCreatorFunction | | conversion/upcasthelpers.ts | MarkerFromAttributeCreatorFunction | UpcastMarkerFromAttributeCreatorFunction | | conversion/viewconsumable.ts | ViewElementConsumables | \_ViewElementConversionConsumables | | conversion/viewconsumable.ts | normalizeConsumables | \_normalizeConversionConsumables | | dataprocessor/basichtmlwriter.ts | BasicHtmlWriter | \_DataProcessorBasicHtmlWriter | | dataprocessor/htmlwriter.ts | HtmlWriter | DataProcessorHtmlWriter | | dev-utils/model.ts | getData | \_getModelData | | dev-utils/model.ts | setData | \_setModelData | | dev-utils/model.ts | stringify | \_stringifyModel | | dev-utils/model.ts | parse | \_parseModel | | dev-utils/operationreplayer.ts | OperationReplayer | \_OperationReplayer | | dev-utils/utils.ts | convertMapToTags | \_convertMapToTags | | dev-utils/utils.ts | convertMapToStringifiedObject | \_convertMapToStringifiedObject | | dev-utils/utils.ts | dumpTrees | \_dumpTrees | | dev-utils/utils.ts | initDocumentDumping | \_initDocumentDumping | | dev-utils/utils.ts | logDocument | \_logDocument | | dev-utils/view.ts | getData | \_getViewData | | dev-utils/view.ts | setData | \_setViewData | | dev-utils/view.ts | stringify | \_stringifyView | | dev-utils/view.ts | parse | \_parseView | | model/differ.ts | DifferSnapshot | \_DifferSnapshot | | model/differ.ts | DiffItem | DifferItem | | model/differ.ts | DiffItemInsert | DifferItemInsert | | model/differ.ts | DiffItemRemove | DifferItemRemove | | model/differ.ts | DiffItemAttribute | DifferItemAttribute | | model/differ.ts | DiffItemRoot | DifferItemRoot | | model/document.ts | Document | ModelDocument | | model/document.ts | DocumentChangeEvent | ModelDocumentChangeEvent | | model/documentfragment.ts | DocumentFragment | ModelDocumentFragment | | model/documentselection.ts | DocumentSelection | ModelDocumentSelection | | model/documentselection.ts | DocumentSelectionChangeRangeEvent | ModelDocumentSelectionChangeRangeEvent | | model/documentselection.ts | DocumentSelectionChangeAttributeEvent | ModelDocumentSelectionChangeAttributeEvent | | model/documentselection.ts | DocumentSelectionChangeMarkerEvent | ModelDocumentSelectionChangeMarkerEvent | | model/documentselection.ts | DocumentSelectionChangeEvent | ModelDocumentSelectionChangeEvent | | model/element.ts | Element | ModelElement | | model/item.ts | Item | ModelItem | | model/liveposition.ts | LivePosition | ModelLivePosition | | model/liveposition.ts | LivePositionChangeEvent | ModelLivePositionChangeEvent | | model/liverange.ts | LiveRange | ModelLiveRange | | model/liverange.ts | LiveRangeChangeRangeEvent | ModelLiveRangeChangeRangeEvent | | model/liverange.ts | LiveRangeChangeContentEvent | ModelLiveRangeChangeContentEvent | | model/liverange.ts | LiveRangeChangeEvent | ModelLiveRangeChangeEvent | | model/model.ts | BeforeChangesEvent | \_ModelBeforeChangesEvent | | model/model.ts | AfterChangesEvent | \_ModelAfterChangesEvent | | model/node.ts | Node | ModelNode | | model/node.ts | NodeAttributes | ModelNodeAttributes | | model/nodelist.ts | NodeList | ModelNodeList | | model/operation/detachoperation.ts | DetachOperation | \_DetachOperation | | model/operation/transform.ts | transform | \_operationTransform | | model/operation/transform.ts | transformSets | transformOperationSets | | model/operation/transform.ts | TransformSetsResult | TransformOperationSetsResult | | model/operation/transform.ts | TransformationContext | \_OperationTransformationContext | | model/operation/utils.ts | \_insert | \_insertIntoModelNodeList | | model/operation/utils.ts | \_remove | \_removeFromModelNodeList | | model/operation/utils.ts | \_move | \_moveInModelNodeList | | model/operation/utils.ts | \_setAttribute | \_setAttributeInModelNodeList | | model/operation/utils.ts | \_normalizeNodes | \_normalizeInModelNodeList | | model/operation/utils.ts | NodeSet | ModelNodeSet | | model/position.ts | Position | ModelPosition | | model/position.ts | PositionRelation | ModelPositionRelation | | model/position.ts | PositionOffset | ModelPositionOffset | | model/position.ts | PositionStickiness | ModelPositionStickiness | | model/position.ts | getTextNodeAtPosition | \_getModelTextNodeAtPosition | | model/position.ts | getNodeAfterPosition | \_getModelNodeAfterPosition | | model/position.ts | getNodeBeforePosition | \_getModelNodeBeforePosition | | model/range.ts | Range | ModelRange | | model/rootelement.ts | RootElement | ModelRootElement | | model/schema.ts | Schema | ModelSchema | | model/schema.ts | SchemaCheckChildEvent | ModelSchemaCheckChildEvent | | model/schema.ts | SchemaCheckAttributeEvent | ModelSchemaCheckAttributeEvent | | model/schema.ts | SchemaItemDefinition | ModelSchemaItemDefinition | | model/schema.ts | SchemaCompiledItemDefinition | ModelSchemaCompiledItemDefinition | | model/schema.ts | SchemaContext | ModelSchemaContext | | model/schema.ts | SchemaContextDefinition | ModelSchemaContextDefinition | | model/schema.ts | SchemaContextItem | ModelSchemaContextItem | | model/schema.ts | AttributeProperties | ModelAttributeProperties | | model/schema.ts | SchemaAttributeCheckCallback | ModelSchemaAttributeCheckCallback | | model/schema.ts | SchemaChildCheckCallback | ModelSchemaChildCheckCallback | | model/selection.ts | Selection | ModelSelection | | model/selection.ts | SelectionChangeEvent | ModelSelectionChangeEvent | | model/selection.ts | SelectionChangeRangeEvent | ModelSelectionChangeRangeEvent | | model/selection.ts | SelectionChangeAttributeEvent | ModelSelectionChangeAttributeEvent | | model/selection.ts | Selectable | ModelSelectable | | model/selection.ts | PlaceOrOffset | ModelPlaceOrOffset | | model/text.ts | Text | ModelText | | model/textproxy.ts | TextProxy | ModelTextProxy | | model/treewalker.ts | TreeWalker | ModelTreeWalker | | model/treewalker.ts | TreeWalkerValueType | ModelTreeWalkerValueType | | model/treewalker.ts | TreeWalkerValue | ModelTreeWalkerValue | | model/treewalker.ts | TreeWalkerDirection | ModelTreeWalkerDirection | | model/treewalker.ts | TreeWalkerOptions | ModelTreeWalkerOptions | | model/typecheckable.ts | TypeCheckable | ModelTypeCheckable | | model/utils/autoparagraphing.ts | autoParagraphEmptyRoots | \_autoParagraphEmptyModelRoots | | model/utils/autoparagraphing.ts | isParagraphable | \_isParagraphableModelNode | | model/utils/autoparagraphing.ts | wrapInParagraph | \_wrapInParagraphModelNode | | model/utils/deletecontent.ts | deleteContent | \_deleteModelContent | | model/utils/getselectedcontent.ts | getSelectedContent | \_getSelectedModelContent | | model/utils/insertcontent.ts | insertContent | \_insertModelContent | | model/utils/insertobject.ts | insertObject | \_insertModelObject | | model/utils/modifyselection.ts | modifySelection | \_modifyModelSelection | | model/utils/selection-post-fixer.ts | injectSelectionPostFixer | \_injectModelSelectionPostFixer | | model/utils/selection-post-fixer.ts | tryFixingRange | \_tryFixingModelRange | | model/utils/selection-post-fixer.ts | mergeIntersectingRanges | \_mergeIntersectingModelRanges | | model/writer.ts | Writer | ModelWriter | | view/attributeelement.ts | AttributeElement | ViewAttributeElement | | view/containerelement.ts | ContainerElement | ViewContainerElement | | view/containerelement.ts | getFillerOffset | getViewFillerOffset | | view/datatransfer.ts | DataTransfer | ViewDataTransfer | | view/datatransfer.ts | EffectAllowed | ViewEffectAllowed | | view/datatransfer.ts | DropEffect | ViewDropEffect | | view/document.ts | Document | ViewDocument | | view/document.ts | ViewDocumentPostFixer | ViewDocumentPostFixer | | view/document.ts | ChangeType | ViewDocumentChangeType | | view/documentfragment.ts | DocumentFragment | ViewDocumentFragment | | view/documentselection.ts | DocumentSelection | ViewDocumentSelection | | view/domconverter.ts | DomConverter | ViewDomConverter | | view/downcastwriter.ts | DowncastWriter | ViewDowncastWriter | | view/editableelement.ts | EditableElement | ViewEditableElement | | view/element.ts | Element | ViewElement | | view/element.ts | ElementAttributeValue | ViewElementAttributeValue | | view/element.ts | ElementAttributes | ViewElementAttributes | | view/element.ts | NormalizedConsumables | ViewNormalizedConsumables | | view/elementdefinition.ts | ElementObjectDefinition | ViewElementObjectDefinition | | view/elementdefinition.ts | ElementDefinition | ViewElementDefinition | | view/emptyelement.ts | EmptyElement | ViewEmptyElement | | view/filler.ts | NBSP\_FILLER | \_VIEW\_NBSP\_FILLER | | view/filler.ts | MARKED\_NBSP\_FILLER | \_VIEW\_MARKED\_NBSP\_FILLER | | view/filler.ts | BR\_FILLER | \_VIEW\_BR\_FILLER | | view/filler.ts | INLINE\_FILLER\_LENGTH | \_VIEW\_INLINE\_FILLER\_LENGTH | | view/filler.ts | INLINE\_FILLER | \_VIEW\_INLINE\_FILLER | | view/filler.ts | startsWithFiller | \_startsWithViewFiller | | view/filler.ts | isInlineFiller | \_isInlineViewFiller | | view/filler.ts | getDataWithoutFiller | \_getDataWithoutViewFiller | | view/filler.ts | injectQuirksHandling | \_injectViewQuirksHandling | | view/item.ts | Item | ViewItem | | view/matcher.ts | isPatternMatched | \_isViewPatternMatched | | view/matcher.ts | PropertyPatterns | MatchPropertyPatterns | | view/matcher.ts | AttributePatterns | MatchAttributePatterns | | view/matcher.ts | StylePatterns | MatchStylePatterns | | view/matcher.ts | ClassPatterns | MatchClassPatterns | | view/matcher.ts | NormalizedPropertyPattern | \_ViewNormalizedPropertyPattern | | view/node.ts | Node | ViewNode | | view/observer/arrowkeysobserver.ts | ArrowKeysObserver | ViewDocumentArrowKeyEvent | | view/observer/bubblingeventinfo.ts | EventPhase | BubblingEventPhase | | view/observer/compositionobserver.ts | CompositionEventData | ViewDocumentCompositionEventData | | view/observer/domeventdata.ts | DomEventData | ViewDocumentDomEventData | | view/observer/inputobserver.ts | InputEventData | ViewDocumentInputEventData | | view/observer/keyobserver.ts | KeyEventData | ViewDocumentKeyEventData | | view/observer/mutationobserver.ts | MutationsEventData | ViewDocumentMutationEventData | | view/observer/mutationobserver.ts | MutationData | ObserverMutationData | | view/placeholder.ts | enablePlaceholder | enableViewPlaceholder | | view/placeholder.ts | disablePlaceholder | disableViewPlaceholder | | view/placeholder.ts | showPlaceholder | showViewPlaceholder | | view/placeholder.ts | hidePlaceholder | hideViewPlaceholder | | view/placeholder.ts | needsPlaceholder | needsViewPlacegolder | | view/placeholder.ts | PlaceholderableElement | PlaceholderableViewElement | | view/position.ts | Position | ViewPosition | | view/position.ts | PositionRelation | ViewPositionRelation | | view/position.ts | PositionOffset | ViewPositionOffset | | view/range.ts | Range | ViewRange | | view/rawelement.ts | RawElement | ViewRawElement | | view/renderer.ts | Renderer | ViewRenderer | | view/rooteditableelement.ts | RootEditableElement | ViewRootEditableElement | | view/selection.ts | Selection | ViewSelection | | view/selection.ts | SelectionOptions | ViewSelectionOptions | | view/selection.ts | PlaceOrOffset | ViewPlaceOrOffset | | view/selection.ts | Selectable | ViewSelectable | | view/styles/background.ts | addBackgroundRules | addBackgroundStylesRules | | view/styles/border.ts | addBorderRules | addBorderStylesRules | | view/styles/margin.ts | addMarginRules | addMarginStylesRules | | view/styles/padding.ts | addPaddingRules | addPaddingStylesRules | | view/styles/utils.ts | isColor | isColorStyleValue | | view/styles/utils.ts | isLineStyle | isLineStyleValue | | view/styles/utils.ts | isLength | isLengthStyleValue | | view/styles/utils.ts | isPercentage | isPercentageStyleValue | | view/styles/utils.ts | isRepeat | isRepeatStyleValue | | view/styles/utils.ts | isPosition | isPositionStyleValue | | view/styles/utils.ts | isAttachment | isAttachmentStyleValue | | view/styles/utils.ts | isURL | isURLStyleValue | | view/styles/utils.ts | getBoxSidesValues | getBoxSidesStyleValues | | view/styles/utils.ts | getBoxSidesValueReducer | getBoxSidesStyleValueReducer | | view/styles/utils.ts | getBoxSidesShorthandValue | getBoxSidesStyleShorthandValue | | view/styles/utils.ts | getPositionShorthandNormalizer | getPositionStyleShorthandNormalizer | | view/styles/utils.ts | getShorthandValues | getShorthandStylesValues | | view/stylesmap.ts | PropertyDescriptor | StylePropertyDescriptor | | view/stylesmap.ts | BoxSides | BoxStyleSides | | view/stylesmap.ts | Normalizer | StylesNormalizer | | view/stylesmap.ts | Extractor | StylesExtractor | | view/stylesmap.ts | Reducer | StylesReducer | | view/text.ts | Text | ViewText | | view/textproxy.ts | TextProxy | ViewTextProxy | | view/tokenlist.ts | TokenList | ViewTokenList | | view/treewalker.ts | TreeWalker | ViewTreeWalker | | view/treewalker.ts | TreeWalkerValueType | ViewTreeWalkerValueType | | view/treewalker.ts | TreeWalkerValue | ViewTreeWalkerValue | | view/treewalker.ts | TreeWalkerDirection | ViewTreeWalkerDirection | | view/treewalker.ts | TreeWalkerOptions | ViewTreeWalkerOptions | | view/typecheckable.ts | TypeCheckable | ViewTypeCheckable | | view/uielement.ts | UIElement | ViewUIElement | | view/uielement.ts | injectUiElementHandling | \_injectViewUIElementHandling | | view/upcastwriter.ts | UpcastWriter | ViewUpcastWriter | | view/view.ts | View | EditingView | | view/view.ts | AlwaysRegisteredObservers | AlwaysRegisteredViewObservers | #### @ckeditor/ckeditor5-enter | file | original name | re-exported name | | ---------------- | ------------------------ | -------------------------- | | enterobserver.ts | EnterObserver | EnterObserver | | enterobserver.ts | EnterEventData | ViewDocumentEnterEventData | | utils.ts | getCopyOnEnterAttributes | \_getCopyOnEnterAttributes | #### @ckeditor/ckeditor5-export-word | file | original name | re-exported name | | ------------- | ------------------------------------------------- | --------------------------------------------------- | | exportword.ts | ExportWordConverterInternalOptions | \_ExportWordConverterInternalOptions | | exportword.ts | ExportWordConverterInternalOptionsV2 | \_ExportWordConverterInternalOptionsV2 | | exportword.ts | ExportWordConverterCollaborationFeaturesOptionsV2 | \_ExportWordConverterCollaborationFeaturesOptionsV2 | | exportword.ts | ExportWordConverterCommentsThreadOptionsV2 | \_ExportWordConverterCommentsThreadOptionsV2 | | exportword.ts | ExportWordConverterCommentsV2 | \_ExportWordConverterCommentsV2 | | exportword.ts | ExportWordConverterSuggestionsOptionsV2 | \_ExportWordConverterSuggestionsOptionsV2 | | exportword.ts | ExportWordConverterMergeFieldsOptionsV2 | \_ExportWordConverterMergeFieldsOptionsV2 | #### @ckeditor/ckeditor5-find-and-replace | File | original name | re-exported name | | ---------------------- | ---------------------------------- | ---------------------------------- | | findandreplace.ts | ResultType | FindResultType | | findandreplacestate.ts | sortSearchResultsByMarkerPositions | \_sortFindResultsByMarkerPositions | | findandreplaceui.ts | SearchResetedEvent | FindResetedEvent | | replacecommandbase.ts | ReplaceCommandBase | FindReplaceCommandBase | #### @ckeditor/ckeditor5-font | File | original name | re-exported name | | ------------------- | -------------------------- | -------------------------------- | | fontfamily/utils.ts | normalizeOptions | \_normalizeFontFamilyOptions | | fontsize/utils.ts | normalizeOptions | \_normalizeFontSizeOptions | | ui/colorui.ts | ColorUI | FontColorUIBase | | utils.ts | buildDefinition | \_buildFontDefinition | | utils.ts | renderUpcastAttribute | \_renderUpcastFontColorAttribute | | utils.ts | renderDowncastElement | \_renderDowncastFontElement | | utils.ts | addColorSelectorToDropdown | \_addFontColorSelectorToDropdown | | utils.ts | ColorSelectorDropdownView | FontColorSelectorDropdownView | #### @ckeditor/ckeditor5-fullscreen | File | original name | re-exported name | | ---------------------------------- | ---------------------- | -------------------------------- | | handlers/abstracteditorhandler.ts | AbstractEditorHandler | FullscreenAbstractEditorHandler | | handlers/classiceditorhandler.ts | ClassicEditorHandler | FullscreenClassicEditorHandler | | handlers/decouplededitorhandler.ts | DecoupledEditorHandler | FullscreenDecoupledEditorHandler | #### @ckeditor/ckeditor5-heading | File | original name | re-exported name | | -------- | ------------------- | ---------------------------- | | title.ts | TitleConfig | HeadingTitleConfig | | utils.ts | getLocalizedOptions | \_getLocalizedHeadingOptions | #### @ckeditor/ckeditor5-html-embed | File | original name | re-exported name | | ------------------- | ------------- | ----------------- | | htmlembedediting.ts | RawHtmlApi | \_RawHtmlEmbedApi | #### @ckeditor/ckeditor5-html-support | file | original name | re-exported name | | -------------------------------- | -------------------------------------- | --------------------------------------------------- | | converters.ts | viewToModelObjectConverter | \_viewToModelObjectContentHtmlSupportConverter | | converters.ts | toObjectWidgetConverter | \_toObjectWidgetHtmlSupportConverter | | converters.ts | createObjectView | \_createObjectHtmlSupportView | | converters.ts | viewToAttributeInlineConverter | \_viewToAttributeInlineHtmlSupportConverter | | converters.ts | emptyInlineModelElementToViewConverter | \_emptyInlineModelElementToViewHtmlSupportConverter | | converters.ts | attributeToViewInlineConverter | \_attributeToInlineHtmlSupportConverter | | converters.ts | viewToModelBlockAttributeConverter | \_viewToModelBlockAttributeHtmlSupportConverter | | converters.ts | modelToViewBlockAttributeConverter | \_modelToViewBlockAttributeHtmlSupportConverter | | datafilter.ts | DataFilterRegisterEvent | HtmlSupportDataFilterRegisterEvent | | dataschema.ts | DataSchemaDefinition | HtmlSupportDataSchemaDefinition | | dataschema.ts | DataSchemaBlockElementDefinition | HtmlSupportDataSchemaBlockElementDefinition | | dataschema.ts | DataSchemaInlineElementDefinition | HtmlSupportDataSchemaInlineElementDefinition | | generalhtmlsupportconfig.ts | FullPageConfig | GHSFullPageConfig | | generalhtmlsupportconfig.ts | CssSanitizeOutput | GHSCssSanitizeOutput | | integrations/integrationutils.ts | getDescendantElement | \_getHtmlSupportDescendantElement | | schemadefinitions.ts | \_HTML\_SUPPORT\_SCHEMA\_DEFINITIONS | | | utils.ts | updateViewAttributes | \_updateHtmlSupportViewAttributes | | utils.ts | setViewAttributes | \_setHtmlSupportViewAttributes | | utils.ts | removeViewAttributes | \_removeHtmlSupportViewAttributes | | utils.ts | mergeViewElementAttributes | \_mergeHtmlSupportViewElementAttributes | | utils.ts | modifyGhsAttribute | \_modifyHtmlSupportGhsAttribute | | utils.ts | toPascalCase | \_toHtmlSupportPascalCase | | utils.ts | getHtmlAttributeName | \_getHtmlSupportAttributeName | #### @ckeditor/ckeditor5-image | file | original name | re-exported name | | -------------------------------------------------------- | ----------------------------------------- | -------------------------------------------- | | image/converters.ts | upcastImageFigure | \_upcastImageFigure | | image/converters.ts | upcastPicture | \_upcastImagePicture | | image/converters.ts | downcastSrcsetAttribute | \_downcastImageSrcsetAttribute | | image/converters.ts | downcastSourcesAttribute | \_downcastImageSourcesAttribute | | image/converters.ts | downcastImageAttribute | \_downcastImageAttribute | | image/imageloadobserver.ts | ImageLoadObserver | ImageLoadObserver | | image/ui/utils.ts | repositionContextualBalloon | \_repositionImageContextualBalloon | | image/ui/utils.ts | getBalloonPositionData | \_getImageBalloonPositionData | | image/utils.ts | createInlineImageViewElement | \_createInlineImageViewElement | | image/utils.ts | createBlockImageViewElement | \_createBlockImageViewElement | | image/utils.ts | getImgViewElementMatcher | \_getImageViewElementMatcher | | image/utils.ts | determineImageTypeForInsertionAtSelection | \_determineImageTypeForInsertionAtSelection | | image/utils.ts | getSizeValueIfInPx | \_getImageSizeValueIfInPx | | image/utils.ts | widthAndHeightStylesAreBothSet | \_checkIfImageWidthAndHeightStylesAreBothSet | | imageinsert/ui/imageinsertformview.ts | ImageInsertFormView | \_ImageInsertFormView | | imageinsert/ui/imageinserturlview.ts | ImageInsertUrlView | \_ImageInsertUrlView | | imageresize/ui/imagecustomresizeformview.ts | ImageCustomResizeFormView | \_ImageCustomResizeFormView | | imageresize/ui/imagecustomresizeformview.ts | ImageCustomResizeFormValidatorCallback | \_ImageCustomResizeFormValidatorCallback | | imageresize/utils/getselectedimageeditornodes.ts | getSelectedImageEditorNodes | \_getSelectedImageEditorNodes | | imageresize/utils/getselectedimagepossibleresizerange.ts | getSelectedImagePossibleResizeRange | \_getSelectedImagePossibleResizeRange | | imageresize/utils/getselectedimagepossibleresizerange.ts | PossibleResizeImageRange | \_PossibleResizeImageRange | | imageresize/utils/getselectedimagewidthinunits.ts | getSelectedImageWidthInUnits | \_getSelectedImageWidthInUnits | | imageresize/utils/tryparsedimensionwithunit.ts | tryParseDimensionWithUnit | \_tryParseImageDimensionWithUnit | | imageresize/utils/tryparsedimensionwithunit.ts | tryCastDimensionsToUnit | \_tryCastImageDimensionsToUnit | | imageresize/utils/tryparsedimensionwithunit.ts | DimensionWithUnit | \_ImageDimensionWithUnit | | imagestyle/converters.ts | modelToViewStyleAttribute | \_modelToViewImageStyleAttribute | | imagestyle/converters.ts | viewToModelStyleAttribute | \_viewToModelImageStyleAttribute | | imagestyle/utils.ts | DEFAULT\_OPTIONS | \_IMAGE\_DEFAULT\_OPTIONS | | imagestyle/utils.ts | DEFAULT\_ICONS | \_IMAGE\_DEFAULT\_ICONS | | imagestyle/utils.ts | DEFAULT\_DROPDOWN\_DEFINITIONS | \_IMAGE\_DEFAULT\_DROPDOWN\_DEFINITIONS | | imagestyle/utils.ts | \_ImageStyleUtils | | | imagetextalternative/ui/textalternativeformview.ts | TextAlternativeFormView | \_ImageTextAlternativeFormView | | imagetextalternative/ui/textalternativeformview.ts | TextAlternativeFormViewSubmitEvent | \_ImageTextAlternativeFormViewSubmitEvent | | imagetextalternative/ui/textalternativeformview.ts | TextAlternativeFormViewCancelEvent | \_ImageTextAlternativeFormViewCancelEvent | | imageupload/imageuploadediting.ts | isHtmlIncluded | isHtmlInDataTransfer | | imageupload/utils.ts | fetchLocalImage | \_fetchLocalImage | | imageupload/utils.ts | isLocalImage | \_isLocalImage | #### @ckeditor/ckeditor5-import-word | file | original name | re-exported name | | -------------------- | ----------------- | --------------------------- | | importword.ts | FormattingOptions | ImportWordFormattingOptions | | importwordcommand.ts | DataInsertEvent | ImportWordDataInsertEvent | #### @ckeditor/ckeditor5-indent | file | original name | re-exported name | | ------------------------------------------- | ------------------ | -------------------- | | indentcommandbehavior/indentusingclasses.ts | IndentUsingClasses | \_IndentUsingClasses | | indentcommandbehavior/indentusingoffset.ts | IndentUsingOffset | \_IndentUsingOffset | #### @ckeditor/ckeditor5-language | file | original name | re-exported name | | -------- | -------------------------- | ---------------------------- | | utils.ts | stringifyLanguageAttribute | \_stringifyLanguageAttribute | | utils.ts | parseLanguageAttribute | \_parseLanguageAttribute | #### @ckeditor/ckeditor5-link | file | original name | re-exported name | | --------------------------- | ------------------------ | ---------------------------- | | ui/linkbuttonview.ts | LinkButtonView | \_LinkButtonView | | ui/linkformview.ts | SubmitEvent | LinkFormSubmitEvent | | ui/linkformview.ts | CancelEvent | LinkFormCancelEvent | | ui/linkpreviewbuttonview.ts | LinkPreviewButtonView | \_LinkPreviewButtonView | | ui/linkpropertiesview.ts | BackEvent | LinkPropertiesBackEvent | | ui/linkprovideritemsview.ts | CancelEvent | LinkProvidersCancelEvent | | utils.ts | LINK\_KEYSTROKE | \_LINK\_KEYSTROKE | | utils.ts | createLinkElement | \_createLinkElement | | utils.ts | ensureSafeUrl | \_ensureSafeLinkUrl | | utils.ts | getLocalizedDecorators | \_getLocalizedLinkDecorators | | utils.ts | normalizeDecorators | \_normalizeLinkDecorators | | utils.ts | isEmail | \_isEmailLink | | utils.ts | linkHasProtocol | \_hasLinkProtocol | | utils.ts | openLink | \_openLink | | utils.ts | extractTextFromLinkRange | \_extractTextFromLinkRange | | utils/manualdecorator.ts | ManualDecorator | LinkManualDecorator | #### @ckeditor/ckeditor5-list | file | original name | re-exported name | | --------------------------------------- | ---------------------------------- | ------------------------------------- | | list/converters.ts | listItemUpcastConverter | \_listItemUpcastConverter | | list/converters.ts | reconvertItemsOnDataChange | \_reconvertListItemsOnDataChange | | list/converters.ts | listItemDowncastConverter | \_listItemDowncastConverter | | list/converters.ts | listItemDowncastRemoveConverter | \_listItemDowncastRemoveConverter | | list/converters.ts | bogusParagraphCreator | \_listItemBogusParagraphCreator | | list/converters.ts | findMappedViewElement | \_findMappedListItemViewElement | | list/converters.ts | createModelToViewPositionMapper | \_createModelToViewListPositionMapper | | list/listcommand.ts | ListCommandAfterExecuteEvent | \_ListCommandAfterExecuteEvent | | list/listediting.ts | ListItemAttributesMap | \_ListItemAttributesMap | | list/listediting.ts | ListEditingCheckAttributesEvent | \_ListEditingCheckAttributesEvent | | list/listediting.ts | ListEditingCheckElementEvent | \_ListEditingCheckElementEvent | | list/listindentcommand.ts | ListIndentCommandAfterExecuteEvent | \_ListIndentCommandAfterExecuteEvent | | list/listmergecommand.ts | ListMergeCommandAfterExecuteEvent | \_ListMergeCommandAfterExecuteEvent | | list/listsplitcommand.ts | ListSplitCommandAfterExecuteEvent | \_ListSplitCommandAfterExecuteEvent | | list/utils.ts | createUIComponents | \_createListUIComponents | | list/utils/listwalker.ts | ListWalker | \_ListWalker | | list/utils/listwalker.ts | SiblingListBlocksIterator | \_SiblingListBlocksIterator | | list/utils/listwalker.ts | ListBlocksIterable | \_ListBlocksIterable | | list/utils/listwalker.ts | ListIteratorValue | \_ListIteratorValue | | list/utils/listwalker.ts | ListWalkerOptions | \_ListWalkerOptions | | list/utils/model.ts | ListItemUid | \_ListItemUid | | list/utils/model.ts | ListElement | \_ListElement | | list/utils/model.ts | isListItemBlock | \_isListItemBlock | | list/utils/model.ts | getAllListItemBlocks | \_getAllListItemBlocks | | list/utils/model.ts | getListItemBlocks | \_getListItemBlocks | | list/utils/model.ts | getNestedListBlocks | \_getNestedListBlocks | | list/utils/model.ts | getListItems | \_getListItems | | list/utils/model.ts | isFirstBlockOfListItem | \_isFirstBlockOfListItem | | list/utils/model.ts | isLastBlockOfListItem | \_isLastBlockOfListItem | | list/utils/model.ts | expandListBlocksToCompleteItems | \_expandListBlocksToCompleteItems | | list/utils/model.ts | expandListBlocksToCompleteList | \_expandListBlocksToCompleteList | | list/utils/model.ts | splitListItemBefore | \_splitListItemBefore | | list/utils/model.ts | mergeListItemBefore | \_mergeListItemBefore | | list/utils/model.ts | indentBlocks | \_indentListBlocks | | list/utils/model.ts | outdentBlocksWithMerge | \_outdentListBlocksWithMerge | | list/utils/model.ts | removeListAttributes | \_removeListAttributes | | list/utils/model.ts | isSingleListItem | \_isSingleListItem | | list/utils/model.ts | outdentFollowingItems | \_outdentFollowingListItems | | list/utils/model.ts | sortBlocks | \_sortListBlocks | | list/utils/model.ts | getSelectedBlockObject | \_getSelectedBlockObject | | list/utils/model.ts | canBecomeSimpleListItem | \_canBecomeSimpleListItem | | list/utils/model.ts | isNumberedListType | \_isNumberedListType | | list/utils/postfixers.ts | findAndAddListHeadToMap | \_findAndAddListHeadToMap | | list/utils/postfixers.ts | fixListIndents | \_fixListIndents | | list/utils/postfixers.ts | fixListItemIds | \_fixListItemIds | | list/utils/view.ts | isListView | \_isListView | | list/utils/view.ts | isListItemView | \_isListItemView | | list/utils/view.ts | getIndent | \_getListIndent | | list/utils/view.ts | createListElement | \_createListElement | | list/utils/view.ts | createListItemElement | \_createListItemElement | | list/utils/view.ts | getViewElementNameForListType | \_getViewElementNameForListType | | list/utils/view.ts | getViewElementIdForListType | \_getViewElementIdForListType | | listproperties/converters.ts | listPropertiesUpcastConverter | \_listPropertiesUpcastConverter | | listproperties/listpropertiesediting.ts | AttributeStrategy | \_ListAttributeConversionStrategy | | listproperties/ui/listpropertiesview.ts | ListPropertiesView | \_ListPropertiesView | | listproperties/ui/listpropertiesview.ts | StylesView | \_ListPropertiesStylesView | | listproperties/utils/config.ts | getNormalizedConfig | \_getNormalizedListConfig | | listproperties/utils/config.ts | NormalizedListPropertiesConfig | \_NormalizedListPropertiesConfig | | listproperties/utils/style.ts | getAllSupportedStyleTypes | \_getAllSupportedListStyleTypes | | listproperties/utils/style.ts | getListTypeFromListStyleType | \_getListTypeFromListStyleType | | listproperties/utils/style.ts | getListStyleTypeFromTypeAttribute | \_getListStyleTypeFromTypeAttribute | | listproperties/utils/style.ts | getTypeAttributeFromListStyleType | \_getTypeAttributeFromListStyleType | | listproperties/utils/style.ts | normalizeListStyle | \_normalizeListStyle | | todolist/todocheckboxchangeobserver.ts | TodoCheckboxChangeObserver | \_TodoCheckboxChangeObserver | #### @ckeditor/ckeditor5-list-multi-level | file | original name | re-exported name | | ----------------- | ------------------------------ | -------------------------------- | | multilevellist.ts | MultiLevelListConfig | \_MultiLevelListConfig | | multilevellist.ts | MultiLevelListDefinition | \_MultiLevelListDefinition | | multilevellist.ts | MultiLevelListMarkerDefinition | \_MultiLevelListMarkerDefinition | | multilevellist.ts | MultiLevelListMarkerPattern | \_MultiLevelListMarkerPattern | #### @ckeditor/ckeditor5-markdown-gfm | file | original name | re-exported name | | ------------------- | ---------------- | ------------------------ | | gfmdataprocessor.ts | GFMDataProcessor | MarkdownGfmDataProcessor | | html2markdown.ts | HtmlToMarkdown | MarkdownGfmHtmlToMd | | markdown2html.ts | MarkdownToHtml | MarkdownGfmMdToHtml | #### @ckeditor/ckeditor5-media-embed | file | original name | re-exported name | | ------------------- | -------------------------------- | --------------------------------------- | | converters.ts | modelToViewUrlAttributeConverter | \_modelToViewUrlAttributeMediaConverter | | ui/mediaformview.ts | MediaFormView | \_MediaFormView | | utils.ts | toMediaWidget | \_toMediaWidget | | utils.ts | getSelectedMediaViewWidget | \_getSelectedMediaViewWidget | | utils.ts | isMediaWidget | \_isMediaWidget | | utils.ts | createMediaFigureElement | \_createMediaFigureElement | | utils.ts | getSelectedMediaModelWidget | \_getSelectedMediaModelWidget | | utils.ts | insertMedia | \_insertMedia | #### @ckeditor/ckeditor5-mention | file | original name | re-exported name | | -------------------- | ---------------------- | --------------------------- | | mentionconfig.ts | FeedCallback | MentionFeedbackCallback | | mentionconfig.ts | ItemRenderer | MentionItemRenderer | | mentionediting.ts | \_addMentionAttributes | \_addMentionAttributes | | mentionediting.ts | \_toMentionAttribute | \_toMentionAttribute | | mentionui.ts | createRegExp | \_createMentionMarkerRegExp | | ui/domwrapperview.ts | DomWrapperView | MentionDomWrapperView | #### @ckeditor/ckeditor5-merge-fields | file | original name | re-exported name | | -------------------- | ----------------- | ---------------------------- | | mergefieldsconfig.ts | GroupDefinition | MergeFieldsGroupDefinition | | mergefieldsconfig.ts | DataSetDefinition | MergeFieldsDataSetDefinition | #### @ckeditor/ckeditor5-minimap | file | original name | re-exported name | | ----------------------------- | -------------------------- | -------------------------------- | | minimapiframeview.ts | MinimapIframeView | \_MinimapIframeView | | minimappositiontrackerview.ts | MinimapPositionTrackerView | \_MinimapPositionTrackerView | | minimapview.ts | MinimapViewOptions | \_MinimapViewOptions | | minimapview.ts | MinimapView | \_MinimapView | | utils.ts | cloneEditingViewDomRoot | \_cloneMinimapEditingViewDomRoot | | utils.ts | getPageStyles | \_getMinimapPageStyles | | utils.ts | getDomElementRect | \_getMinimapDomElementRect | | utils.ts | getClientHeight | \_getMinimapClientHeight | | utils.ts | getScrollable | \_getMinimapScrollable | #### @ckeditor/ckeditor5-paste-from-office | file | original name | re-exported name | | ------------------------------------- | -------------------------------------- | --------------------------------------------------- | | filters/bookmark.ts | transformBookmarks | \_transformPasteOfficeBookmarks | | filters/br.ts | transformBlockBrsToParagraphs | \_transformPasteOfficeBlockBrsToParagraphs | | filters/image.ts | replaceImagesSourceWithBase64 | \_replacePasteOfficeImagesSourceWithBase64 | | filters/image.ts | \_convertHexToBase64 | \_convertHexToBase64 | | filters/list.ts | transformListItemLikeElementsIntoLists | \_transformPasteOfficeListItemLikeElementsIntoLists | | filters/list.ts | unwrapParagraphInListItem | \_unwrapPasteOfficeParagraphInListItem | | filters/parse.ts | parseHtml | parsePasteOfficeHtml | | filters/parse.ts | ParseHtmlResult | PasteOfficeHtmlParseResult | | filters/removeboldwrapper.ts | removeBoldWrapper | \_removePasteOfficeBoldWrapper | | filters/removegooglesheetstag.ts | removeGoogleSheetsTag | \_removePasteGoogleOfficeSheetsTag | | filters/removeinvalidtablewidth.ts | removeInvalidTableWidth | \_removePasteOfficeInvalidTableWidths | | filters/removemsattributes.ts | removeMSAttributes | \_removePasteMSOfficeAttributes | | filters/removestyleblock.ts | removeStyleBlock | \_removePasteOfficeStyleBlock | | filters/removexmlns.ts | removeXmlns | \_removePasteOfficeXmlnsAttributes | | filters/space.ts | normalizeSpacing | \_normalizePasteOfficeSpacing | | filters/space.ts | normalizeSpacerunSpans | \_normalizePasteOfficeSpaceRunSpans | | filters/table.ts | transformTables | \_transformPasteOfficeTables | | filters/utils.ts | convertCssLengthToPx | \_convertPasteOfficeCssLengthToPx | | filters/utils.ts | isPx | \_isPasteOfficePxValue | | filters/utils.ts | toPx | \_toPasteOfficePxValue | | normalizer.ts | Normalizer | PasteFromOfficeNormalizer | | normalizer.ts | NormalizerData | PasteFromOfficeNormalizerData | | normalizers/googledocsnormalizer.ts | GoogleDocsNormalizer | PasteFromOfficeGoogleDocsNormalizer | | normalizers/googlesheetsnormalizer.ts | GoogleSheetsNormalizer | PasteFromOfficeGoogleSheetsNormalizer | | normalizers/mswordnormalizer.ts | MSWordNormalizer | PasteFromOfficeMSWordNormalizer | #### @ckeditor/ckeditor5-real-time-collaboration | file | original name | re-exported name | | ------------------------------------------------ | ------------------------------- | ------------------------------------ | | config.ts | PresenceListConfig | RtcPresenceListConfig | | presencelist/view/presencedropdownlistview.ts | PresenceDropdownListView | \_RtcPresenceDropdownListView | | presencelist/view/presencedropdownlistview.ts | PresenceDropdownListWrapperView | \_RtcPresenceDropdownListWrapperView | | presencelist/view/presencelistview.ts | PresenceListView | \_RtcPresenceListView | | realtimecollaborativeediting/sessions.ts | ServerUser | RtcServerUser | | realtimecollaborativeediting/sessions.ts | SessionAddEvent | RtcSessionAddEvent | | realtimecollaborativeediting/websocketgateway.ts | WebSocketGateway | RtcWebSocketGateway | | realtimecollaborativeediting/websocketgateway.ts | ReconnectPlugin | RtcReconnectPlugin | | realtimecollaborativeediting/websocketgateway.ts | ReconnectContextPlugin | RtcReconnectContextPlugin | #### @ckeditor/ckeditor5-restricted-editing | file | original name | re-exported name | | ----------------------------------- | --------------------------------- | ---------------------------------------------------- | | restrictededitingmode/converters.ts | setupExceptionHighlighting | \_setupRestrictedEditingExceptionHighlighting | | restrictededitingmode/converters.ts | resurrectCollapsedMarkerPostFixer | \_resurrectRestrictedEditingCollapsedMarkerPostFixer | | restrictededitingmode/converters.ts | extendMarkerOnTypingPostFixer | \_extendRestrictedEditingMarkerOnTypingPostFixer | | restrictededitingmode/converters.ts | upcastHighlightToMarker | \_upcastRestrictedEditingHighlightToMarker | | restrictededitingmode/utils.ts | getMarkerAtPosition | \_getRestrictedEditingMarkerAtPosition | | restrictededitingmode/utils.ts | isPositionInRangeBoundaries | \_isRestrictedEditingPositionInRangeBoundaries | | restrictededitingmode/utils.ts | isSelectionInMarker | \_isRestrictedEditingSelectionInMarker | #### @ckeditor/ckeditor5-revision-history | file | original name | re-exported name | | ------------------ | -------------- | -------------------------- | | revisionhistory.ts | TapeValue | \_RevisionHistoryTapeValue | | revisionhistory.ts | TapeItem | \_RevisionHistoryTapeItem | | revisiontracker.ts | RevisionSource | \_RevisionHistorySource | #### @ckeditor/ckeditor5-special-characters | file | original name | re-exported name | | ------------------------------------- | ------------------------------- | --------------------------------------- | | ui/charactergridview.ts | CharacterGridView | \_SpecialCharactersGridView | | ui/charactergridview.ts | CharacterGridViewExecuteEvent | SpecialCharactersGridViewExecuteEvent | | ui/charactergridview.ts | CharacterGridViewTileHoverEvent | SpecialCharactersGridViewTileHoverEvent | | ui/charactergridview.ts | CharacterGridViewTileFocusEvent | SpecialCharactersGridViewTileFocusEvent | | ui/charactergridview.ts | CharacterGridViewEventData | SpecialCharactersGridViewEventData | | ui/characterinfoview.ts | CharacterInfoView | \_SpecialCharacterInfoView | | ui/specialcharacterscategoriesview.ts | SpecialCharactersCategoriesView | \_SpecialCharactersCategoriesView | | ui/specialcharactersview.ts | SpecialCharactersView | \_SpecialCharactersView | #### @ckeditor/ckeditor5-style | file | original name | re-exported name | | ------------------------- | ----------------------------------------------- | ----------------------------------------------- | | styleutils.ts | StyleUtilsIsEnabledForBlockEvent | StyleUtilsIsEnabledForBlockEvent | | styleutils.ts | StyleUtilsIsActiveForBlockEvent | StyleUtilsIsActiveForBlockEvent | | styleutils.ts | StyleUtilsGetAffectedBlocksEvent | StyleUtilsGetAffectedBlocksEvent | | styleutils.ts | StyleUtilsIsStyleEnabledForInlineSelectionEvent | StyleUtilsIsStyleEnabledForInlineSelectionEvent | | styleutils.ts | StyleUtilsIsStyleActiveForInlineSelectionEvent | StyleUtilsIsStyleActiveForInlineSelectionEvent | | styleutils.ts | StyleUtilsGetAffectedInlineSelectableEvent | StyleUtilsGetAffectedInlineSelectableEvent | | styleutils.ts | StyleUtilsGetStylePreviewEvent | StyleUtilsGetStylePreviewEvent | | styleutils.ts | StyleUtilsConfigureGHSDataFilterEvent | StyleUtilsConfigureGHSDataFilterEvent | | ui/stylegridbuttonview.ts | StyleGridButtonView | \_StyleGridButtonView | | ui/stylegridview.ts | StyleGridView | \_StyleGridView | | ui/stylegroupview.ts | StyleGroupView | \_StyleGroupView | | ui/stylepanelview.ts | StylePanelView | \_StylePanelView | #### @ckeditor/ckeditor5-table | file | original name | re-exported name | | --------------------------------------------- | -------------------------------------- | -------------------------------------------- | | converters/downcast.ts | downcastTable | \_downcastTable | | converters/downcast.ts | downcastRow | \_downcastTableRow | | converters/downcast.ts | downcastCell | \_downcastTableCell | | converters/downcast.ts | convertParagraphInTableCell | \_convertParagraphInTableCell | | converters/downcast.ts | isSingleParagraphWithoutAttributes | \_isSingleTableParagraphWithoutAttributes | | converters/downcast.ts | DowncastTableOptions | \_DowncastTableOptions | | converters/table-caption-post-fixer.ts | injectTableCaptionPostFixer | \_injectTableCaptionPostFixer | | converters/table-cell-paragraph-post-fixer.ts | injectTableCellParagraphPostFixer | \_injectTableCellParagraphPostFixer | | converters/table-cell-refresh-handler.ts | tableCellRefreshHandler | \_tableCellRefreshHandler | | converters/table-headings-refresh-handler.ts | tableHeadingsRefreshHandler | \_tableHeadingsRefreshHandler | | converters/table-layout-post-fixer.ts | injectTableLayoutPostFixer | \_injectTableLayoutPostFixer | | converters/tableproperties.ts | upcastStyleToAttribute | \_upcastNormalizedTableStyleToAttribute | | converters/tableproperties.ts | StyleValues | \_TableStyleValues | | converters/tableproperties.ts | upcastBorderStyles | \_upcastTableBorderStyles | | converters/tableproperties.ts | downcastAttributeToStyle | \_downcastTableAttributeToStyle | | converters/tableproperties.ts | downcastTableAttribute | \_downcastTableAttribute | | converters/tableproperties.ts | getDefaultValueAdjusted | \_getDefaultTableValueAdjusted | | converters/upcasttable.ts | upcastTableFigure | \_upcastTableFigure | | converters/upcasttable.ts | upcastTable | \_upcastTable | | converters/upcasttable.ts | skipEmptyTableRow | \_skipEmptyTableRow | | converters/upcasttable.ts | ensureParagraphInTableCell | \_ensureParagraphInTableCell | | tablecaption/utils.ts | isTable | \_isTableModelElement | | tablecaption/utils.ts | getCaptionFromTableModelElement | \_getTableCaptionFromModelElement | | tablecaption/utils.ts | getCaptionFromModelSelection | \_getTableCaptionFromModelSelection | | tablecaption/utils.ts | matchTableCaptionViewElement | \_matchTableCaptionViewElement | | tablecolumnresize/constants.ts | COLUMN\_MIN\_WIDTH\_AS\_PERCENTAGE | \_TABLE\_COLUMN\_MIN\_WIDTH\_AS\_PERCENTAGE | | tablecolumnresize/constants.ts | COLUMN\_MIN\_WIDTH\_IN\_PIXELS | \_TABLE\_COLUMN\_MIN\_WIDTH\_IN\_PIXELS | | tablecolumnresize/constants.ts | COLUMN\_WIDTH\_PRECISION | \_TABLE\_COLUMN\_WIDTH\_PRECISION | | tablecolumnresize/constants.ts | COLUMN\_RESIZE\_DISTANCE\_THRESHOLD | \_TABLE\_COLUMN\_RESIZE\_DISTANCE\_THRESHOLD | | tablecolumnresize/converters.ts | upcastColgroupElement | \_upcastTableColgroupElement | | tablecolumnresize/converters.ts | downcastTableResizedClass | \_downcastTableResizedClass | | tablecolumnresize/utils.ts | getColumnMinWidthAsPercentage | \_getTableColumnMinWidthAsPercentage | | tablecolumnresize/utils.ts | getTableWidthInPixels | \_getTableWidthInPixels | | tablecolumnresize/utils.ts | getElementWidthInPixels | \_getElementWidthInPixels | | tablecolumnresize/utils.ts | getColumnEdgesIndexes | \_getTableColumnEdgesIndexes | | tablecolumnresize/utils.ts | toPrecision | \_toPrecision | | tablecolumnresize/utils.ts | clamp | \_clamp | | tablecolumnresize/utils.ts | createFilledArray | \_createFilledArray | | tablecolumnresize/utils.ts | sumArray | \_sumArray | | tablecolumnresize/utils.ts | normalizeColumnWidths | \_normalizeTableColumnWidths | | tablecolumnresize/utils.ts | getDomCellOuterWidth | \_getDomTableCellOuterWidth | | tablecolumnresize/utils.ts | updateColumnElements | \_updateTableColumnElements | | tablecolumnresize/utils.ts | getColumnGroupElement | \_getTableColumnGroupElement | | tablecolumnresize/utils.ts | getTableColumnElements | \_getTableColumnElements | | tablecolumnresize/utils.ts | getTableColumnsWidths | \_getTableColumnsWidths | | tablecolumnresize/utils.ts | translateColSpanAttribute | \_translateTableColspanAttribute | | tableediting.ts | AdditionalSlot | TableConversionAdditionalSlot | | tablemouse/mouseeventsobserver.ts | MouseEventsObserver | \_TableMouseEventsObserver | | tablemouse/mouseeventsobserver.ts | ViewDocumentMouseMoveEvent | ViewDocumentTableMouseMoveEvent | | tablemouse/mouseeventsobserver.ts | ViewDocumentMouseLeaveEvent | ViewDocumentTableMouseLeaveEvent | | ui/colorinputview.ts | ColorInputViewOptions | \_TableColorInputViewOptions | | ui/colorinputview.ts | ColorInputView | \_TableColorInputView | | ui/inserttableview.ts | InsertTableView | \_InsertTableView | | utils/common.ts | updateNumericAttribute | \_updateTableNumericAttribute | | utils/common.ts | createEmptyTableCell | \_createEmptyTableCell | | utils/common.ts | isHeadingColumnCell | \_isHeadingColumnCell | | utils/common.ts | enableProperty | \_enableTableCellProperty | | utils/common.ts | getSelectionAffectedTable | \_getSelectionAffectedTable | | utils/structure.ts | cropTableToDimensions | \_cropTableToDimensions | | utils/structure.ts | getVerticallyOverlappingCells | \_getVerticallyOverlappingTableCells | | utils/structure.ts | splitHorizontally | \_splitTableCellHorizontally | | utils/structure.ts | getHorizontallyOverlappingCells | \_getHorizontallyOverlappingTableCells | | utils/structure.ts | splitVertically | \_splitTableCellVertically | | utils/structure.ts | trimTableCellIfNeeded | \_trimTableCellIfNeeded | | utils/structure.ts | removeEmptyColumns | \_removeEmptyTableColumns | | utils/structure.ts | removeEmptyRows | \_removeEmptyTableRows | | utils/structure.ts | removeEmptyRowsColumns | \_removeEmptyTableRowsColumns | | utils/structure.ts | adjustLastRowIndex | \_adjustLastTableRowIndex | | utils/structure.ts | adjustLastColumnIndex | \_adjustLastTableColumnIndex | | utils/table-properties.ts | getSingleValue | \_getTableBorderBoxSingleValue | | utils/table-properties.ts | addDefaultUnitToNumericValue | \_addDefaultUnitToNumericValue | | utils/table-properties.ts | NormalizedDefaultProperties | \_NormalizedTableDefaultProperties | | utils/table-properties.ts | NormalizeTableDefaultPropertiesOptions | \_NormalizeTableDefaultPropertiesOptions | | utils/table-properties.ts | getNormalizedDefaultProperties | \_getNormalizedDefaultTableBaseProperties | | utils/table-properties.ts | getNormalizedDefaultTableProperties | \_getNormalizedDefaultTableProperties | | utils/table-properties.ts | getNormalizedDefaultCellProperties | \_getNormalizedDefaultTableCellProperties | | utils/ui/contextualballoon.ts | repositionContextualBalloon | \_repositionTableContextualBalloon | | utils/ui/contextualballoon.ts | getBalloonTablePositionData | \_getBalloonTablePositionData | | utils/ui/contextualballoon.ts | getBalloonCellPositionData | \_getBalloonTableCellPositionData | | utils/ui/table-properties.ts | getBorderStyleLabels | \_getBorderTableStyleLabels | | utils/ui/table-properties.ts | getLocalizedColorErrorText | \_getLocalizedTableColorErrorText | | utils/ui/table-properties.ts | getLocalizedLengthErrorText | \_getLocalizedTableLengthErrorText | | utils/ui/table-properties.ts | colorFieldValidator | \_colorTableFieldValidator | | utils/ui/table-properties.ts | lengthFieldValidator | \_lengthTableFieldValidator | | utils/ui/table-properties.ts | lineWidthFieldValidator | \_lineWidthTableFieldValidator | | utils/ui/table-properties.ts | getBorderStyleDefinitions | \_getTableOrCellBorderStyleDefinitions | | utils/ui/table-properties.ts | fillToolbar | \_fillTableOrCellToolbar | | utils/ui/table-properties.ts | defaultColors | \_TABLE\_DEFAULT\_COLORS | | utils/ui/table-properties.ts | getLabeledColorInputCreator | \_getLabeledTableColorInputCreator | | utils/ui/widget.ts | getSelectionAffectedTableWidget | \_getSelectionAffectedTableWidget | | utils/ui/widget.ts | getSelectedTableWidget | \_getSelectedTableWidget | | utils/ui/widget.ts | getTableWidgetAncestor | \_getTableWidgetAncestor | --- #### @ckeditor/ckeditor5-track-changes | file | original name | re-exported name | | ---------------------------------- | ----------------------- | ---------------------------------------- | | suggestiondescriptionfactory.ts | Description | SuggestionDescription | | suggestiondescriptionfactory.ts | DescriptionCallback | SuggestionDescriptionCallback | | suggestiondescriptionfactory.ts | LabelCallback | SuggestionLabelCallback | | suggestiondescriptionfactory.ts | LabelCallbackObject | \_SuggestionLabelCallbackObject | | trackchangesediting.ts | renameAttributeKey | \_TRACK\_CHANGES\_RENAME\_ATTRIBUTE\_KEY | | trackchangesediting.ts | FormatData | SuggestionFormatData | | trackchangesediting.ts | AttributeData | SuggestionAttributeData | | ui/view/trackchangespreviewview.ts | TrackChangesPreviewView | \_SuggestionsPreviewView | --- #### @ckeditor/ckeditor5-typing | file | original name | re-exported name | | --------------------- | ------------------------------------ | ------------------------------------------ | | deleteobserver.ts | DeleteObserver | \_DeleteObserver | | inserttextobserver.ts | InsertTextObserver | InsertTextObserver | | textwatcher.ts | TextWatcherMatchedDataEventData | TextWatcherMatchedTypingDataEventData | | textwatcher.ts | TextWatcherMatchedSelectionEvent | TextWatcherMatchedTypingSelectionEvent | | textwatcher.ts | TextWatcherMatchedSelectionEventData | TextWatcherMatchedTypingSelectionEventData | | textwatcher.ts | TextWatcherUnmatchedEvent | TextWatcherUnmatchedTypingEvent | | typingconfig.ts | TextTransformationDescription | TextTypingTransformationDescription | | utils/changebuffer.ts | ChangeBuffer | TypingChangeBuffer | --- #### @ckeditor/ckeditor5-ui | file | original name | re-exported name | | -------------------------------------- | ---------------------------------------- | ------------------------------------------ | | bindings/preventdefault.ts | preventDefault | \_preventUiViewDefault | | colorpicker/colorpickerview.ts | tryParseHexColor | \_tryNormalizeHexColor | | colorpicker/utils.ts | convertColor | \_convertColor | | colorpicker/utils.ts | convertToHex | \_convertColorToHex | | colorpicker/utils.ts | registerCustomElement | \_registerCustomElement | | dropdown/menu/dropdownmenubehaviors.ts | DropdownRootMenuBehaviors | \_DropdownRootMenuBehaviors | | dropdown/menu/dropdownmenubehaviors.ts | DropdownMenuBehaviors | \_DropdownMenuBehaviors | | menubar/utils.ts | MenuBarBehaviors | \_MenuBarBehaviors | | menubar/utils.ts | MenuBarMenuBehaviors | \_MenuBarMenuBehaviors | | menubar/utils.ts | MenuBarMenuViewPanelPositioningFunctions | \_MenuBarMenuViewPanelPositioningFunctions | | menubar/utils.ts | processMenuBarConfig | \_processMenuBarConfig | | model.ts | Model | UIModel | | panel/balloon/contextualballoon.ts | RotatorView | \_ContextualBalloonRotatorView | | search/searchinfoview.ts | SearchInfoView | \_SearchInfoView | | search/text/searchtextqueryview.ts | SearchTextQueryView | \_SearchTextQueryView | | template.ts | RenderData | \_TemplateRenderData | | toolbar/toolbarview.ts | NESTED\_TOOLBAR\_ICONS | NESTED\_TOOLBAR\_ICONS | | toolbar/toolbarview.ts | ToolbarBehavior | \_ToolbarBehavior | #### @ckeditor/ckeditor5-undo | file | original name | re-exported name | | -------------- | ------------- | ------------------- | | basecommand.ts | BaseCommand | UndoRedoBaseCommand | #### @ckeditor/ckeditor5-upload | file | original name | re-exported name | | ------------- | ------------- | ---------------- | | filereader.ts | FileReader | FileReader | #### @ckeditor/ckeditor5-utils | file | original name | re-exported name | | -------------------------------- | -------------------------------- | ---------------------------------- | | areconnectedthroughproperties.ts | areConnectedThroughProperties | areConnectedThroughProperties | | ckeditorerror.ts | DOCUMENTATION\_URL | DOCUMENTATION\_URL | | dom/getcommonancestor.ts | getCommonAncestor | getCommonAncestor | | dom/getpositionedancestor.ts | getPositionedAncestor | getPositionedAncestor | | dom/global.ts | GlobalType | GlobalType | | dom/global.ts | globalVar | global | | dom/iswindow.ts | isWindow | isWindow | | dom/position.ts | Options | DomOptimalPositionOptions | | dom/position.ts | PositioningFunctionResult | DomPositioningFunctionResult | | dom/rect.ts | RectLike | DomRectLike | | env.ts | getUserAgent | \_getUserAgent | | env.ts | isMac | \_isMac | | env.ts | isWindows | \_isWindows | | env.ts | isGecko | \_isGecko | | env.ts | isSafari | \_isSafari | | env.ts | isiOS | \_isiOS | | env.ts | isAndroid | \_isAndroid | | env.ts | isBlink | \_isBlink | | env.ts | isRegExpUnicodePropertySupported | \_isRegExpUnicodePropertySupported | | env.ts | isMediaForcedColors | \_isMediaForcedColors | | env.ts | isMotionReduced | \_isMotionReduced | | mapsequal.ts | mapsEqual | mapsEqual | | nth.ts | nth | nth | | objecttomap.ts | objectToMap | objectToMap | | spy.ts | spy | spy | | translation-service.ts | \_clear | \_clearTranslations | #### @ckeditor/ckeditor5-watchdog | file | original name | re-exported name | | ------------------ | ------------------------- | -------------------------------- | | contextwatchdog.ts | WatchdogItemConfiguration | ContextWatchdogItemConfiguration | #### @ckeditor/ckeditor5-widget | file | original name | re-exported name | | ---------------------------- | ---------------------------------- | -------------------------------------------- | | highlightstack.ts | HighlightStack | WidgetHighlightStack | | highlightstack.ts | HighlightStackChangeEvent | WidgetHighlightStackChangeEvent | | highlightstack.ts | HighlightStackChangeEventData | WidgetHighlightStackChangeEventData | | verticalnavigation.ts | verticalNavigationHandler | verticalWidgetNavigationHandler | | widgetresize.ts | ResizerOptions | WidgetResizerOptions | | widgetresize/resizer.ts | Resizer | WidgetResizer | | widgetresize/resizer.ts | ResizerBeginEvent | WidgetResizerBeginEvent | | widgetresize/resizer.ts | ResizerCancelEvent | WidgetResizerCancelEvent | | widgetresize/resizer.ts | ResizerCommitEvent | WidgetResizerCommitEvent | | widgetresize/resizer.ts | ResizerUpdateSizeEvent | WidgetResizerUpdateSizeEvent | | widgetresize/resizerstate.ts | ResizeState | WidgetResizeState | | widgetresize/sizeview.ts | SizeView | \_WidgetSizeView | | widgettypearound/utils.ts | TYPE\_AROUND\_SELECTION\_ATTRIBUTE | \_WIDGET\_TYPE\_AROUND\_SELECTION\_ATTRIBUTE | | widgettypearound/utils.ts | isTypeAroundWidget | isTypeAroundWidget | | widgettypearound/utils.ts | getClosestTypeAroundDomButton | \_getClosestWidgetTypeAroundDomButton | | widgettypearound/utils.ts | getTypeAroundButtonPosition | \_getWidgetTypeAroundButtonPosition | | widgettypearound/utils.ts | getClosestWidgetViewElement | \_getClosestWidgetViewElement | | widgettypearound/utils.ts | getTypeAroundFakeCaretPosition | \_getWidgetTypeAroundFakeCaretPosition | #### @ckeditor/ckeditor5-word-count | file | original name | re-exported name | | -------- | ----------------------- | ------------------------- | | utils.ts | modelElementToPlainText | \_modelElementToPlainText | ### New exports Listed below are exports introduced with NIM. #### @ckeditor/ckeditor5-ai | file | exported name | | ----------------------------- | -------------------- | | adapters/openaitextadapter.ts | AIRequestMessageItem | #### @ckeditor/ckeditor5-case-change | file | exported name | | -------------------- | --------------------------- | | casechangecommand.ts | CaseChangeTransformCallback | #### @ckeditor/ckeditor5-collaboration-core | file | exported name | | -------- | ---------------------- | | users.ts | CollaborationUserColor | | users.ts | CollaborationUserData | #### @ckeditor/ckeditor5-comments | file | exported name | | ------------------------------ | ------------------------ | | annotations/annotation.ts | AnnotationTargetBase | | comments/commentsrepository.ts | CommentPermissionsConfig | #### @ckeditor/ckeditor5-import-word | file | exported name | | -------------------- | ----------------------------- | | importwordcommand.ts | ImportWordDataInsertEventData | #### @ckeditor/ckeditor5-real-time-collaboration | file | exported name | | ------------------------------------------------ | ------------- | | realtimecollaborativeediting/websocketgateway.ts | RtcReconnect | #### @ckeditor/ckeditor5-track-changes | file | exported name | | ------------------------------- | ------------------------- | | suggestiondescriptionfactory.ts | SuggestionDescriptionItem | | trackchangesconfig.ts | TrackChangesPreviewConfig | | trackchangesdata.ts | TrackChangesDataGetter | #### @ckeditor/ckeditor5-uploadcare | file | exported name | | -------------------------------------------- | ---------------------- | | uploadcareconfig.ts | UploadcareExcludedKeys | | uploadcareimageedit/uploadcareimageeditui.ts | UploadcareImageCache | #### @ckeditor/ckeditor5-engine | file | exported name | | -------------------------------- | -------------------------- | | conversion/conversion.ts | ConversionType | | conversion/downcastdispatcher.ts | DowncastDispatcherEventMap | | view/domconverter.ts | ViewBlockFillerMode | #### @ckeditor/ckeditor5-table | file | exported name | | ------------- | ------------------ | | tableutils.ts | TableIndexesObject | #### @ckeditor/ckeditor5-ui | file | exported name | | ----------------------------------------- | -------------------------------- | | arialiveannouncer.ts | AriaLiveAnnouncerPolitenessValue | | arialiveannouncer.ts | AriaLiveAppendContentAttributes | | arialiveannouncer.ts | AriaLiveAnnounceConfig | | button/filedialogbuttonview.ts | FileDialogViewMixin | | button/filedialogbuttonview.ts | FileDialogButtonViewBase | | colorpicker/colorpickerview.ts | SliderView | | colorpicker/colorpickerview.ts | ColorPickerInputRowView | | editableui/inline/inlineeditableuiview.ts | InlineEditableUIViewOptions | | template.ts | AttributeValues | | toolbar/toolbarview.ts | ItemsView | | editorui/poweredby.ts | PoweredByConfig | #### @ckeditor/ckeditor5-utils | file | exported name | | -------------------- | ------------------------- | | crc32.ts | CRCValue | | dom/createelement.ts | HTMLElementAttributes | | dom/createelement.ts | SVGElementAttributes | | dom/createelement.ts | ChildrenElements | | dom/scroll.ts | IfTrue | | observablemixin.ts | ObservableSingleBindChain | | observablemixin.ts | ObservableDualBindChain | | observablemixin.ts | ObservableMultiBindChain | source file: "ckeditor5/latest/updating/nim-migration/migration-to-new-installation-methods.html" ## Migrating to new installation methods In this guide, we will explore the new installation methods introduced in CKEditor 5 v42.0.0\. These methods make CKEditor 5 much easier to use by reducing the number of possible installation paths and removing most of the limitations of the old methods. Links to migration guides for specific installation methods can be found in the table of contents on the left or under the **main menu button in the upper-left corner** on mobile and at the end of this document. Let’s start by comparing the new installation methods to the old ones to better understand what has changed. ### Legacy installation methods Prior to version 42.0.0, there were several ways to install CKEditor 5, each with its own limitations and quirks that made it difficult or impossible to use in certain scenarios. It was also difficult for us to properly document all possible setups without making the documentation overly complex, as these setups were so different from each other. Here is a code example showing one of the possible setups using the old installation methods: ``` // webpack.config.js const path = require( 'path' ); const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' ); const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); module.exports = { entry: './src/index.js', output: { path: path.resolve( __dirname, 'dist' ), filename: 'bundle.js' }, plugins: [ new CKEditorTranslationsPlugin( { language: 'en' } ) ], module: { rules: [ { test: /\.svg$/, use: [ 'raw-loader' ] }, { test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, use: [ { loader: 'style-loader', options: { injectType: 'singletonStyleTag', attributes: { 'data-cke': true } } }, 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: styles.getPostCssConfig( { minify: true } ) } } ] } ] } }; ``` ``` // src/index.js import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; import { Essentials } from '@ckeditor/ckeditor5-essentials'; import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { Mention } from '@ckeditor/ckeditor5-mention'; import { FormatPainter } from '@ckeditor/ckeditor5-format-painter'; import { SlashCommand } from '@ckeditor/ckeditor5-slash-command'; ClassicEditor .create( { attachTo: document.querySelector( '#editor' ), plugins: [ Essentials, Bold, Italic, Paragraph, Mention, FormatPainter, SlashCommand ], toolbar: [ /* ... */ ], licenseKey: '', // This value must be kept in sync with the language defined in webpack.config.js. language: 'en' } ); ``` It may seem strange to show the webpack configuration in an example of the old installation methods, but it was a necessary part of the setup to handle translations, CSS, and SVG files. This setup could be even more complex if you wanted to use TypeScript. ### New installation methods In the new installation methods we have reduced the number of possible paths to just two: **npm packages and browser builds**. Unlike before, both methods no longer require you to add dozens of individual packages or JavaScript bundles to get the editor up and running. Instead, you can import the editor and all our open source plugins from the `ckeditor5` package and the premium features from `ckeditor5-premium-features`. You also do not need to worry about a specific webpack or Vite configurations, as the new installation methods are designed to work out-of-the-box with any modern bundler or JavaScript meta-framework like Next.js. #### npm packages The new npm packages are the recommended way to install CKEditor 5 if you use a module bundler like Vite or webpack or one of the popular JavaScript meta-frameworks. This is what the new npm setup looks like when using the open-source and commercial features and translations: ``` import { ClassicEditor, Essentials, Bold, Italic, Paragraph, Mention } from 'ckeditor5'; import { FormatPainter, SlashCommand } from 'ckeditor5-premium-features'; import coreTranslations from 'ckeditor5/translations/pl.js'; import premiumFeaturesTranslations from 'ckeditor5-premium-features/translations/pl.js'; import 'ckeditor5/ckeditor5.css'; import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; ClassicEditor .create( { attachTo: document.querySelector( '#editor' ), plugins: [ Essentials, Bold, Italic, Paragraph, Mention, FormatPainter, SlashCommand ], toolbar: [ /* ... */ ], licenseKey: '', translations: [ coreTranslations, premiumFeaturesTranslations ] } ); ``` #### Browser builds The browser builds are a great way to use CKEditor 5 if you do not want to build JavaScript with a module bundler. The browser builds are available as JavaScript modules and can be loaded directly in the browser using the ` ``` In some environments, you may not be able to use the import maps or JavaScript modules. In such cases, you can use the UMD builds instead. These register global variables that you can use in your scripts. This is the same setup as above, but using the UMD builds: ``` ``` #### What’s new? There are a few things that stand out in both examples compared to the old installation methods: 1. Everything is imported from the `ckeditor5` and `ckeditor5-premium-features` packages only. In the browser, this is done using [importmaps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), which maps the package names to the build URLs. 2. CSS files are imported separately from the JavaScript files, which improves performance and allows you to more easily customize or remove the default editor styles. 3. Translations are imported as JavaScript objects and passed to the editor instance, instead of using side-effect imports (`import '...'`) that rely on the global state. 4. You no longer need to maintain a CKEditor 5-specific webpack or Vite configuration, and can use CKEditor 5 with any modern bundler or JavaScript meta-framework. The setups we presented above are what you should aim for when migrating your project to the new installation methods. #### Feature comparison Here is a visual comparison of the features available in the new npm and CDN builds and the old installation methods: | Installation methods | New methods | Legacy methods | | | | | ---------------------------------------------- | ----------- | -------------- | ------ | -------- | - | | npm | CDN | Predefined | Custom | DLL | | | No build step | ❌ | ✅ | ✅ | ❌ | ✅ | | Can be used with any modern bundler | ✅ | ✅ | ✅ | ❌ | ❌ | | Allows adding plugins | ✅ | ✅ | ❌ | ✅ | ✅ | | Style customization | ✅ | ✅ | ❌ | ⚠️ \[1\] | ❌ | | Icon customization | ✅ | ❌ | ❌ | ✅ | ❌ | | Does not rely on global state | ✅ | ✅ | ❌ | ❌ | ❌ | | Provides editor- and content-only style sheets | ✅ | ✅ | ❌ | ❌ | ❌ | | Style sheets separate from JavaScript | ✅ | ✅ | ❌ | ⚠️\[2\] | ❌ | | Can be optimized to reduce bundle size | ✅ | ❌ | ❌ | ✅ | ✅ | \[1\] Style customization is partially supported via webpack configuration. \[2\] CSS can be separated from JavaScript using custom webpack configuration. ### Sunset of old installation methods and deprecation timelines With the release of version 42.0.0, we have decided to deprecate the older methods of setting up CKEditor 5\. The new experience introduced in v42.0.0 is far superior. However, we understand that migrating to a new setup, even if easy, requires planning and work allocation. We would rather not block anyone from receiving bug fixes and improvements due to a deprecated update path. Therefore, we will support all existing methods according to the timelines given below. #### Deprecation of the predefined builds Our provided predefined editor builds, such as `ckeditor5-build-classic`, were supported until **the end of Q1 (March), 2025**. What we sunset on this date: 1. The documentation for the predefined builds and superbuild was removed. 2. No more new versions of predefined builds packages are published to npm. 3. We updated our environment to target ES2022, thus dropping the support for webpack 4. See the [migration guide](#ckeditor5/latest/updating/nim-migration/predefined-builds.html). #### Deprecation of the custom builds The setup method which was “webpack-first”, in which you imported from specific packages from `src` folder, will be supported until **the end of Q1 (March), 2026**. What we will sunset on this date: 1. The documentation for the custom builds will be removed. 2. New npm package versions will no longer include the `src` directory. Instead, the `dist` directory will become the primary entry point for importing files, and all imports will happen through the package’s index. 3. Deprecation of `@ckeditor/ckeditor5-dev-translations` package, as it will not be needed anymore. 4. Still to be decided, but we may deprecate loading translations from the `CKEDITOR_TRANSLATIONS` global, as new installation methods enable and promote doing it through the editor’s configuration. See the [migration guide](#ckeditor5/latest/updating/nim-migration/customized-builds.html). #### Deprecation of DLLs This is an advanced setup method that we provided, that was used to dynamically create the editor and its configuration on the browser side. As this is now provided out-of-the-box with our browser builds, this method will also be deprecated. As DLLs are used in complex CMSes, this deprecation timeline is significantly longer. The DLLs will be supported until **the end of Q1 (March), 2026**. What we will sunset on this date: 1. The documentation for DLLs will be removed. 2. New versions of npm packages published after this date will not have `build` directory. See the [migration guide](#ckeditor5/latest/updating/nim-migration/dll-builds.html). ### Migrating from the old installation methods To migrate your project to the new installation methods, you can follow the instructions below. First, if you maintain any CKEditor 5 custom plugins as separate packages, whether in a monorepo setup or published to npm, you need to migrate them: * [Migrating custom plugins](#ckeditor5/latest/updating/nim-migration/custom-plugins.html). Second, proceed with migrating your project, depending on the old installation method you are using. * [Migrating from predefined builds](#ckeditor5/latest/updating/nim-migration/predefined-builds.html). * [Migrating from legacy Online Builder](#ckeditor5/latest/updating/nim-migration/online-builder.html). * [Migrating from customized builds](#ckeditor5/latest/updating/nim-migration/customized-builds.html). * [Migrating from DLL builds](#ckeditor5/latest/updating/nim-migration/dll-builds.html). Finally, if you use our React, Vue or Angular integrations, you also need to update them: * Update the `@ckeditor/ckeditor5-react` package to version `^8.0.0`. Please refer to the [package’s changelog](https://github.com/ckeditor/ckeditor5-react/blob/master/CHANGELOG.md), because of the minor breaking change introduced in this version. * Update the `@ckeditor/ckeditor5-vue` package to version `^6.0.0`. * Update the `@ckeditor/ckeditor5-angular` package to version `^8.0.0`. If you encounter any issues during the migration process, please refer to this [GitHub issue containing common errors](https://github.com/ckeditor/ckeditor5/issues/16511). If your issue is not listed there, feel free to open a new issue in our [GitHub repository](https://github.com/ckeditor/ckeditor5/issues/new/choose). source file: "ckeditor5/latest/updating/nim-migration/online-builder.html" ## Migrating from legacy Online Builder There are three installation methods you can migrate to from the legacy Online Builder. The best option for you depends on whether you just want an out-of-the-box browser build, or if you want a customized and optimized build. The npm package is the most flexible and powerful way to install CKEditor 5\. It allows you to create a custom build of the editor with only the features you need, thus significantly reducing the final size of the build. However, you will need a JavaScript bundler or meta-framework to create such a build. If you do not want a build process, you can either use our CDN build or download the ZIP archive. Both of these include the editor and all plugins, so you can use all the features of CKEditor 5 without setting up a build process. ### CDN build The CDN build is a good option to quickly add CKEditor 5 to your website without installing any dependencies or setting up a build process. We recommend using our new interactive [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs) to customize the build to your needs. Then, in the `Installation` section of the Builder, you can select the `Cloud (CDN)` option to learn how to add the editor to your website. ### ZIP archive If you do not want to have a build process or use our CDN build, you can download a ZIP archive with the editor build. We recommend using our new interactive [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs) to customize the build to your needs. Then, in the `Installation` section of the Builder, you can select the `Self-hosted (ZIP)` option to learn how to add the editor to your website. ### npm package If you decide to use the npm package, you can either use our new interactive [Builder](https://ckeditor.com/ckeditor-5/builder/?redirect=docs) to create a new build, or you can update your existing project from the legacy Online Builder. **We recommend using the new interactive Builder**, but if you want to keep your existing build, you can follow the steps below. 1. Follow the steps in the [Migrating from customized builds](#ckeditor5/latest/updating/nim-migration/customized-builds.html) guide. 2. Once this is done, remove the old `build` folder and run the following command to create a new build of CKEditor 5. ``` npm run build ``` 1. There should be three files in the new `build` folder: * `ckeditor.d.ts`, * `ckeditor.js`, * `ckeditor.js.map`. Now you can start to remove some unused webpack plugins and update the `webpack.config.js` file. 2. Uninstall the following `devDependencies`: ``` npm uninstall \ @ckeditor/ckeditor5-dev-translations \ @ckeditor/ckeditor5-dev-utils \ @ckeditor/ckeditor5-theme-lark\ css-loader \ postcss \ postcss-loader \ raw-loader \ style-loader \ terser-webpack-plugin ``` 1. Install the following packages: ``` npm install --save-dev \ css-loader \ css-minimizer-webpack-plugin \ mini-css-extract-plugin \ terser-webpack-plugin ``` 1. Update the `webpack.config.js` file: ``` 'use strict'; /* eslint-env node */ const path = require( 'path' ); const TerserWebpackPlugin = require( 'terser-webpack-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); module.exports = { devtool: 'source-map', performance: { hints: false }, entry: path.resolve( __dirname, 'src', 'ckeditor.ts' ), output: { // The name under which the editor will be exported. library: 'ClassicEditor', path: path.resolve( __dirname, 'build' ), filename: 'ckeditor.js', libraryTarget: 'umd', libraryExport: 'default' }, optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin(), new TerserWebpackPlugin( { terserOptions: { output: { // Preserve CKEditor 5 license comments. comments: /^!/ } }, extractComments: false } ) ] }, plugins: [ new MiniCssExtractPlugin( { filename: 'ckeditor.css' } ), ], resolve: { extensions: [ '.ts', '.js', '.json' ] }, module: { rules: [ { test: /\.ts$/, use: 'ts-loader' }, { test: /\.css$/i, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] } }; ``` 1. Add the following line to the `sample/index.html` file before other CSS files: ``` ``` 1. Delete the old `build` folder and run the following command to create a new build of CKEditor 5. ``` npm run build ``` 1. There should be five files in the new `build` folder: * `ckeditor.css`, * `ckeditor.css.map`, * `ckeditor.d.ts`, * `ckeditor.js`, * `ckeditor.js.map`. The new build has two more files because the CSS is now separated from the JavaScript file, which should improve performance compared to the old approach. When updating your project that uses the `build` folder, remember to import this new CSS file as well. Additionally, both the JavaScript and CSS files are now minified, potentially improving performance. If you want to optimize the build further, follow the steps from the [Optimizing build size](#ckeditor5/latest/getting-started/setup/optimizing-build-size.html) guide. source file: "ckeditor5/latest/updating/nim-migration/predefined-builds.html" ## Migrating from predefined builds Before version 42.0.0, the predefined builds were the easiest way to get started with CKEditor 5\. They provided an out-of-the-box editor with a predefined set of plugins and a default configuration. However, they had limitations, such as the inability to customize the editor by adding or removing plugins. The new installation methods solve this problem. They allow you to fully customize the editor, whether you use npm packages or browser builds. Migrating from the predefined builds to the new installation methods should mostly be a matter of copying and pasting the code below to replace the old code. The code to copy depends on the build and distribution method you used. ### Prerequisites Before you start, follow the usual upgrade path to update your project to use the latest version of CKEditor 5\. This will rule out any problems that may be caused by upgrading from an outdated version of CKEditor 5. ### Migration steps #### npm If you are using predefined builds from npm, follow the steps below: 1. Start by uninstalling the old build package. It can be identified by the `@ckeditor/ckeditor5-build-` prefix. For example, if you were using the `@ckeditor/ckeditor5-build-classic` package, you should uninstall it. Below is the command to uninstall all predefined builds. ``` npm uninstall \ @ckeditor/ckeditor5-build-balloon \ @ckeditor/ckeditor5-build-balloon-block \ @ckeditor/ckeditor5-build-classic \ @ckeditor/ckeditor5-build-decoupled-document \ @ckeditor/ckeditor5-build-inline \ @ckeditor/ckeditor5-build-multi-root ``` 2. Next, install the `ckeditor5` package. This package contains the editor and all of our open-source plugins. ``` npm install ckeditor5 ``` 3. (Optional) If you are using premium features from our commercial offer, you should also install the `ckeditor5-premium-features` package. ``` npm install ckeditor5-premium-features ``` 4. Open the file where you initialized the editor. Then replace the import statement and the initialization code depending on the build you are using. Classic editor Before: ``` import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; ClassicEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { ClassicEditor, Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; class Editor extends ClassicEditor { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` Inline editor Before: ``` import InlineEditor from '@ckeditor/ckeditor5-build-inline'; InlineEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { InlineEditor, Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; class Editor extends InlineEditor { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` Balloon editor Before: ``` import BalloonEditor from '@ckeditor/ckeditor5-build-balloon'; BalloonEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { BalloonEditor, Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; class Editor extends BalloonEditor { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` Balloon block editor Before: ``` import BalloonEditor from '@ckeditor/ckeditor5-build-balloon-block'; BalloonEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { BalloonEditor, Essentials, CKFinderUploadAdapter, Autoformat, BlockToolbar, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; /* Create an additional stylesheet file with the given content: .ck.ck-block-toolbar-button { transform: translateX( calc(-1 * var(--ck-spacing-large)) ); } */ class Editor extends BalloonEditor { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, BlockToolbar, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { blockToolbar: [ 'undo', 'redo', '|', 'heading', '|', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ], toolbar: { items: [ 'bold', 'italic', 'link' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` Decoupled document editor Before: ``` import DecoupledEditor from '@ckeditor/ckeditor5-build-decoupled-document'; DecoupledEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { DecoupledEditor, Essentials, Alignment, FontSize, FontFamily, FontColor, FontBackgroundColor, CKFinderUploadAdapter, Autoformat, Bold, Italic, Strikethrough, Underline, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, IndentBlock, Link, List, ListProperties, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; class Editor extends DecoupledEditor { static builtinPlugins = [ Essentials, Alignment, FontSize, FontFamily, FontColor, FontBackgroundColor, CKFinderUploadAdapter, Autoformat, Bold, Italic, Strikethrough, Underline, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Indent, IndentBlock, Link, List, ListProperties, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'fontfamily', 'fontsize', 'fontColor', 'fontBackgroundColor', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'alignment', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { resizeUnit: 'px', toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, list: { properties: { styles: true, startIndex: true, reversed: true } }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` Multi-root editor Before: ``` import MultiRootEditor from '@ckeditor/ckeditor5-build-multi-root'; MultiRootEditor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` After: ``` import { MultiRootEditor, Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, Table, TableToolbar, TextTransformation, CloudServices } from 'ckeditor5'; import 'ckeditor5/ckeditor5.css'; class Editor extends MultiRootEditor { static builtinPlugins = [ Essentials, CKFinderUploadAdapter, Autoformat, Bold, Italic, BlockQuote, CKBox, CKFinder, CloudServices, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, Indent, Link, List, MediaEmbed, Paragraph, PasteFromOffice, PictureEditing, Table, TableToolbar, TextTransformation ]; static defaultConfig = { toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, language: 'en' }; } Editor .create( /* Configuration */ ) .catch( error => console.error( error ) ); ``` 5. Unlike when using predefined builds, you are now free to customize the editor by adding or removing plugins. However, before you do this, you should test the editor to make sure it works as expected. #### CDN If you are using the predefined builds from CDN, follow the steps below depending on whether you want to use JavaScript modules (ESM) with imports or standard (UMD) scripts with global variables. ##### CDN with imports One notable difference between the old build and the new ESM build is that the former uses the ` ``` 2. Add the `` tags to include the editor’s CSS files and the ` ``` 2.2 If you also use premium features from our commercial offer: ``` ``` 3. Replace the old ` ``` After: ``` ``` Inline editor Before: ``` ``` After: ``` ``` Balloon editor Before: ``` ``` After: ``` ``` Balloon block editor Before: ``` ``` After: ``` ``` Decoupled document editor Before: ``` ``` After: ``` ``` Multi-root editor Before: ``` ``` After: ``` ``` Superbuild Before: ``` ``` After: ``` ``` 4. Unlike when using predefined builds, you are now free to customize the editor by adding or removing plugins. However, before you do this, you should test the editor to make sure it works as expected. ##### CDN with global variables 1. Start by removing the ` ``` 2. Add the `` and ` ``` 2.2 If you also use premium features from our commercial offer: ``` ``` 3. Replace the old ` ``` After: ``` ``` Inline editor Before: ``` ``` After: ``` ``` Balloon editor Before: ``` ``` After: ``` ``` Balloon block editor Before: ``` ``` After: ``` ``` Decoupled document editor Before: ``` ``` After: ``` ``` Multi-root editor Before: ``` ``` After: ``` ``` Superbuild Before: ``` ``` After: ``` ``` 4. Unlike when using predefined builds, you are now free to customize the editor by adding or removing plugins. However, before you do this, you should test the editor to make sure it works as expected. source file: "ckeditor5/latest/updating/technology-upgrades-policy.html" ## Technology upgrades policy This document describes how CKEditor 5 handles upgrades of its core technologies. The goal is to provide a stable, predictable upgrade path for everyone who builds with CKEditor 5 or contributes to it. It covers the following technical areas: * JavaScript and CSS (browser support), * TypeScript, * Node.js, * Framework integrations (Vue, Angular, React). ### Our primary goal Adopt the established best practices of each technology’s community, instead of inventing our own rules. We believe that aligning with well-known standards makes the development process more transparent and easier for plugin authors and for projects that consume CKEditor 5. ### Why this policy exists This policy will help you: * Understand when CKEditor 5 introduces breaking changes, * Prepare your plugins and applications for major upgrades, * Follow stable patterns instead of learning each change in isolation. ### Upgrade frequency and release cadence * CKEditor 5 updates its technology baselines **only in major releases**. * Minor and patch releases do not introduce breaking changes related to tooling or browser support. * This predictable cycle lets the developers plan ahead and avoid unexpected breakages. ### What this means for plugin authors and applications When you upgrade to a new major version of CKEditor 5: * Check the updated minimum version of TypeScript, * If you use our tooling or forked one of our repositories, check the updated minimum version of Node, * Review browser support changes, * Update framework integrations to supported versions, * Follow the corresponding migration guide for detailed instructions. ### Detailed rules #### Impact matrix The table below summarizes how technology-baseline updates affect three key groups: end-users of the editor, projects consuming CKEditor 5, and plugin authors. | Area | End-users of the editor | Projects consuming CKEditor 5 | Plugin authors | | ------------------------------------------------ | ----------------------- | ----------------------------- | -------------- | | **JavaScript and CSS (Browser Support)** | Yes | Yes | Yes | | **TypeScript** | No | Yes | Yes | | **Node.js** | No | No | Yes | | **Framework integrations (Vue, Angular, React)** | No | Yes | No | #### JavaScript and CSS (Browser Support) **Approach:** Once a year, we will update the build target to align with the “Widely available” category defined by the [Baseline web](https://web.dev/baseline) platform standard. **Reasoning:** Driven by major browser vendors like Google and Mozilla, Baseline provides a clear definition of web features that are mature and safe to use. The “Widely available” category, which we will target, includes features stable across all major browsers for at least 30 months. This approach allows us to balance progress with stability, ensuring CKEditor 5 works for the vast majority of users while leveraging modern web platform features. Its status as a reliable standard is reinforced by growing support from developer tools like [MDN](https://developer.mozilla.org/en-US/blog/baseline-unified-view-stable-web-features/), [caniuse](https://caniuse.com/proxy), [Vite](https://vite.dev/blog/announcing-vite7), [ESLint](https://web.dev/blog/eslint-baseline-integration), and [Angular](https://angular.dev/reference/versions#browser-support). #### TypeScript **Approach:** We will follow the [DefinitelyTyped support window](https://github.com/DefinitelyTyped/DefinitelyTyped?tab=readme-ov-file#support-window). This means the minimum supported TypeScript version will be the oldest version that is less than 2 years old, updated approximately every 6 months. **Reasoning:** DefinitelyTyped is the de facto standard for TypeScript’ `(@types/... packages)` type definitions in the entire JavaScript ecosystem. Virtually every typed project relies on it, including CKEditor 5\. By mirroring their support policy, we ensure maximum compatibility and interoperability. #### Node.js **Approach:** We will update the development environment to the [latest Active Long-Term Support (LTS) version of Node.js](https://nodejs.org/en/about/previous-releases) approximately every 6 months. We may update more often if Node releases critical security fixes that impact development or CI environments. **Reasoning:** The Node.js community recommends using either Active LTS or Maintenance LTS for production environments. We chose Active LTS because it provides high stability and security of an LTS release while giving us access to more modern features than versions in the older Maintenance LTS phase. #### Framework integrations (Vue, Angular, React) **Approach:** We will support all officially supported and actively maintained versions of each integration framework. If a library does not publish a clear support window, we will base our decision on usage data, such as community adoption trends and download statistics. **Reasoning:** Our goal is to ensure that CKEditor 5 integrations are compatible with the versions that most developers use without stretching our resources to maintain legacy frameworks that are no longer relevant or safe. This strikes a balance between stability and staying current with modern development practices. source file: "ckeditor5/latest/updating/versioning-policy.html" ## Versioning policy CKEditor 5 is a modular ecosystem of over 80 packages, distributed through npm. To provide predictability and a consistent developer experience, we follow a unified versioning and release policy across all packages. ### Package structure CKEditor 5 is delivered in two core packages: * [**ckeditor5**](https://www.npmjs.com/package/ckeditor5) – the framework and open-source features. * [**ckeditor5-premium-features**](https://www.npmjs.com/package/ckeditor5-premium-features) – commercial plugins and add-ons. Together, these aggregate and version over 80 underlying packages that make up the editor framework, features, and utilities. In addition, the ecosystem provides separately versioned integration packages and tooling: * **Integrations:** [@ckeditor/ckeditor5-react](https://www.npmjs.com/package/@ckeditor/ckeditor5-react), [@ckeditor/ckeditor5-angular](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular), [@ckeditor/ckeditor5-vue](https://www.npmjs.com/package/@ckeditor/ckeditor5-vue) * **Tooling:** [ckeditor5-package-generator](https://www.npmjs.com/package/ckeditor5-package-generator), [@ckeditor/ckeditor5-integrations-common](https://www.npmjs.com/package/@ckeditor/ckeditor5-integrations-common) ### Unified versioning * All CKEditor 5 packages share the **same version number**. * This includes both `ckeditor5` and `ckeditor5-premium-features`, as well as all underlying feature and framework packages. * Integration packages and tooling may follow their own versioning, but major compatibility notes are always documented. This unified versioning approach is common in large ecosystems (for example, Angular). It simplifies dependency management, avoids mismatched versions, and makes it easy to know which packages are compatible. ### Pinned dependencies All packages in the ecosystem pin dependencies to specific versions. This prevents issues with npm/yarn/pnpm resolving past versions incorrectly and guarantees reproducible builds. ### Version numbers We use the **`MAJOR.MINOR.PATCH`** scheme: * **MAJOR** – Introduced when at least one package requires a **major breaking change**. This affects the entire ecosystem. * **MINOR** – Introduced when a package adds a new feature or introduces a **minor breaking change**. * **PATCH** – Introduced when a package only includes bug fixes, internal changes, or documentation updates. Because CKEditor 5 spans multiple layers – from low-level utilities through framework APIs to ready-to-use builds – our approach differs from strict [Semantic Versioning](https://semver.org/). Instead, our policy balances stability with the flexibility needed for such a broad ecosystem. ### Breaking changes Breaking changes are categorized based on which layer of the ecosystem they affect: * **Integration layer** (editor builds, configuration, and top-level APIs): * Breaking changes are considered **major**. * Introduced very rarely and only when unavoidable. * **Plugin development API** (packages such as `@ckeditor/ckeditor5-engine` or `@ckeditor/ckeditor5-core`): * Breaking changes are also considered **major**. * Introduced occasionally, but batched to reduce the number of major releases. * **Low-level customization APIs and feature customization APIs** (internal utilities, hooks, helper functions, and lower-level APIs exposed by specific features, for example the Link balloon): * Designed mainly for **deep customizations of existing features**, rather than for building integrations or plugins. * Treated as **less stable** – breaking changes are considered **minor** and may occur more often as these APIs evolve with feature development. * Provide powerful flexibility but are closer to implementation details, so they should not be relied upon for long-term compatibility guarantees. ### Release schedule We typically publish a **new major release of CKEditor 5 every 6 months**, though in some cases new majors may arrive sooner. Each new major replaces the previous one as the actively supported version, ensuring that all users benefit from the latest improvements, fixes, and compatibility updates. For projects that need **long-term stability**, we also offer the commercial **CKEditor 5 LTS (Long-term Support) Edition**. Every two years, one major release (starting with **v47.0.0**) is designated as an LTS release, providing up to **3 years of guaranteed updates** – 6 months of active development followed by 2.5 years of maintenance with security and critical compatibility fixes. Read more in the [CKEditor 5 LTS Edition](#ckeditor5/latest/getting-started/setup/using-lts-edition.html) guide. ### Release channels CKEditor 5 is distributed through several release channels, each serving a different purpose: * **Stable releases**: The recommended versions for production use. These are fully tested and supported, and are available via **npm**, **ZIP packages**, and the **official CDN**. * **Alpha builds**: Used for early access and testing before a stable release. Alpha builds are published to npm under the `alpha` dist-tag. * **Nightly builds**: Generated automatically from the latest development branch. They are available via npm under the `nightly` dist-tag and are intended for testing the newest changes. If you encounter an issue, please [report it in the CKEditor 5 issue tracker](https://github.com/ckeditor/ckeditor5/issues). Early feedback (especially about alpha and nightly releases) gives us more time to investigate and resolve problems before they reach a stable release. ### Tracking changes To stay up to date with changes: * **Changelog**: Check the [CKEditor 5 changelog](https://github.com/ckeditor/ckeditor5/blob/stable/CHANGELOG.md). * **News**: Read the [CKEditor Ecosystem Blog](https://ckeditor.com/blog/) or subscribe to the [newsletter](http://ckeditor.com/#newsletter-signup). * **npm**: Follow the [ckeditor5](https://www.npmjs.com/package/ckeditor5) and [ckeditor5-premium-features](https://www.npmjs.com/package/ckeditor5-premium-features) packages. ### Update guides When a release introduces breaking or otherwise important changes, the [Updating CKEditor 5](#ckeditor5/latest/updating/index.html) section provides technical details and migration steps. Always review these guides after a release to keep your integration stable. # Cloud Services source file: "cs/latest/developer-resources/apis/authentication.html" ## Authentication ### CKEditor Cloud Services REST API The REST API uses HMAC Authentication. It means that every request must include a signature and a timestamp. The signature needs to be created in accordance with the requirements mentioned in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. #### Examples Detailed examples for Node.js and other programming languages can be found in the [Request Signatures examples](#cs/latest/examples/security/request-signature-nodejs.html) section. ### Converters APIs Converters APIs use JWT to authenticate requests. The generated token should be placed as `Authorization` header. #### Example The token should be generated based on the example below: ``` const jwt = require( 'jsonwebtoken' ); const accessKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy'; const environmentId = 'LJRQ1bju55p6a47RwadH'; const payload = { aud: environmentId }; const token = jwt.sign( payload, accessKey, { algorithm: 'HS256', expiresIn: '24h' } ); console.log( 'Authentication token', token ); ``` Please keep in mind that token generation should be done on a backend side, to avoid exposing `accessKey` to public. Anyone who gets the `accessKey` is able to use converters using your subscription. More detailed examples for Node.js and for other programming language can be found in a [Token endpoints examples](#cs/latest/examples/token-endpoints/nodejs.html) section. ### Next steps Read more about the overall [System security](#cs/latest/guides/system-security.html). source file: "cs/latest/developer-resources/apis/errors.html" ## Errors The CKEditor Collaboration Server REST API uses standard HTTP response codes. ### Error body Each error has the following fields: * `message` – It contains a short error description. * `status_code` – It contains the response status. * `trace_id` – It is a unique request identifier. * `data` – It may contain various information and may have various structure depending on the endpoint. * `explanation` – It contains an explanation why the particular error ocurred. * `action` – In case some action can solve the problem it contains a description of what should be done. ### Example The presented error is an example which can occur while importing comments to a given document, but comments were already imported. ``` { "message": "The target document already contains an import of the comments but it is based on a different snapshot.", "traceId": "e5abd738-db35-4372-b1de-cff9b48cc311", "statusCode": 409, "explanation": "A single target document cannot contain multiple imports of the same source.", "action": "Use a different target document ID.", "data": { "documentId": "doc-1", "importSnapshotAt": "2022-10-19T11:12:21.357Z" } } ``` source file: "cs/latest/developer-resources/apis/overview.html" ## RESTful APIs - overview CKEditor Cloud Services offer several REST APIs that can be used for server integration. The APIs currently include: * **CKEditor Cloud Services Restful APIs** – Provides a full-featured RESTful API that you can use to create a server-to-server integration. * **CKBox Restful API** – Provides an API for managing data stored in the CKBox. * **HTML to PDF Converter API** – Provides an API for converting HTML/CSS documents to PDF format. * **HTML to DOCX Converter API** – Provides an API for converting HTML documents to Microsoft Word `.docx` files. * **DOCX to HTML Converter API** – Provides an API for converting Microsoft Word `.docx`/`.dotx` files to HTML documents. ### Usage Each method can be used for different purposes. For example, the REST API methods for comments allow for synchronizing comments between CKEditor Cloud Services and another system. In addition to that, CKEditor Cloud Services can be used as a database for comments because it is possible to download them via the REST API at the time they are being displayed. An example of using another API method is getting the content of the document from a collaborative editing session. This feature can be used to build an auto-save mechanism for the document, which should reduce transfer costs — auto-save requests are not executed by each connected user but only by the system once at a time. ### Information CKEditor Cloud Services REST APIs provide a lot of powerful methods that make it possible to control and manage data. ### Documentation #### CKEditor Cloud Services Restful APIs The API documentation is available here: . It is an aggregator of all Restful APIs currently available. #### CKBox Restful API The API documentation is available at . Read more about this service in the [CKBox](#ckbox/latest/guides/index.html) guide. #### HTML to PDF Converter API The API documentation is available at . Read more about this service in the [Export to PDF](#cs/latest/guides/export-to-pdf/overview.html) guide. #### HTML to DOCX Converter API The API documentation is available at . Read more about this service in the [Export to Word](#cs/latest/guides/export-to-word/overview.html) guide. #### DOCX to HTML Converter API The API documentation is available at . Read more about this service in the [Import from Word](#cs/latest/guides/import-from-word/overview.html) guide. source file: "cs/latest/developer-resources/easy-image/service-details.html" ## Easy Image This article explains how Easy Image works internally and is geared towards more advanced users. ### Image processing For every uploaded image, Easy Image produces several optimized versions of the same image. Image sizes are calculated in two ways depending on the width of the original image: * For large images (wider than 800px) the width is reduced every 10%. * For small images the width is reduced every 80px until the 100px limit is reached. The original image gets saved in the cloud, too. Thanks to offering several versions of the same image, devices with smaller resolutions, such as mobile devices, may request images that fit their display, reducing the bandwidth and improving the loading time of a website. #### Examples For a 4000px wide image the following versions of it will be created: 400px, 800px, 1200px, 1600px, 2000px, 2400px, 2800px, 3200px, 3600px and 4000px. For a 500px wide image the following versions will be created: 100px, 180px, 260px, 340px, 420px and 500px. ### Upload response The Easy Image service responds with a JSON object containing addresses of generated versions of the image. The keys indicate the width of the image, the original image is named `default`. By default the aspect ratio is preserved. #### Sample response ``` { "390":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_390", "780":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_780", "1170":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_1170", "1560":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_1560", "1950":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_1950", "2340":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_2340", "2730":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_2730", "3120":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_3120", "3510":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_3510", "3840":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg/w_3840", "default":"https://cdn.cke-cs.com/f0pqzdtf0yRhaX1FymZU/images/48f57a98cae2304ef7c8cee5f6ad6741dfd4c9f62873f659_image1.jpg" } ``` The entire communication is hidden from the developer and requires no effort on their part. #### Generated HTML markup The markup used for images varies a bit depending on the editor version, however, the `` element always contains a similar [srcset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-srcset) attribute where all the generated versions of the image are listed: ```
...
...
``` You can find more information about `srcset` and responsive images in the [Responsive images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia%5Fand%5Fembedding/Responsive%5Fimages) article on MDN.
### Requesting a different size of the image The Easy Image service supports requesting any size of an image. #### Parameters To request an image with a custom size, add parameters to the default URL. Parameters are separated by commas. The names of parameters and values should be separated by an underscore, for example `w_300,h_100`. Abbreviated names of parameters must be used, see the table below: | Abbreviated name | Full name | | ---------------- | --------- | | w | width | | h | height | When `w` and `h` are used together, aspect ratio is ignored and a stretched image will be returned. ##### Example A sample default image URL: `https://cdn.cke-cs.com/aX1FymZU/images/48f57_image1.jpg` The same image resized to 200x100px: `https://cdn.cke-cs.com/aX1FymZU/images/48f57_image1.jpg/w_200,h_100` ### Supported file formats Easy Image supports the following formats: * `png` * `jpeg` * `bmp` (images in `bmp` will be converted to `png`) * `tiff` * `webp` * `gif` (animated gifs are also supported) ### Roles for Easy Image There is no need to specify any roles for Easy Image, because they are assigned to the environment. It means that each user that belongs to the environment is allowed to upload images. After the upload, images are public and can be downloaded without any restrictions. ### Integration with CKEditor 5 #### Installation This package is part of our open-source aggregate package. Just install CKEditor 5 to gain access to it. ``` npm install ckeditor5 ``` Then add Easy Image to your plugin list and configure the feature. For instance: ``` import { EasyImage, Image } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ EasyImage, Image, /* ... */ ], toolbar: [ 'uploadImage', /* ... */ ], // Configure the endpoint. See the "Configuration" section above. cloudServices: { tokenUrl: 'https://example.com/cs-token-endpoint', uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/' } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` To make enabling image upload in CKEditor 5 a breeze, the `EasyImage` plugin integrates with the Easy Image service provided by CKEditor Cloud Services. Enabling it is straightforward and the results are immediate. Simply configure CKEditor 5 setting shown above, putting in correct upload URL and Token Endpoint. #### Configuring allowed file types The allowed file types that can be uploaded should actually be configured in two places: * On the client side, in CKEditor 5, restricting image upload through the CKEditor 5 UI and commands. * On the server side, in Easy Image, restricting the file formats that are accepted in Easy Image (as explained above). ##### Client-side configuration Use the `image.upload.types` configuration option to define the allowed image MIME types that can be uploaded to CKEditor 5. By default, users are allowed to upload `jpeg`, `png`, `gif`, `bmp`, `webp` and `tiff` files. This corresponds with file formats supported by Easy Image, but you can modify the list to limit the number of allowed image types. source file: "cs/latest/developer-resources/environments/environments-management.html" ## Environments management You need to create an environment first to work with CKEditor Cloud Services. Environments allow you to create access credentials, manage webhooks, configure features, and connect to CKEditor Cloud Services. ### Managing environments In order to manage your environments, you should log in to the [Customer Portal](https://portal.ckeditor.com) for SaaS or to the Management Panel for On-Premises. The “Overwiev” section lists your active product subscriptions. Select the “Cloud environments” link to manage your environments. #### Creating a new environment To create a new environment, click the “Create environment” button. A modal will show up asking for the name of the new environment. Provide the name and click the “Create environment” button. The newly created environment will show up on the list of environments. #### Removing an environment From the list of environments select the one that you want to remove and click the “Remove” button in the “Actions” column. The modal will show up and will prompt for a confirmation of the environment deletion. Confirm by clicking the “Remove” button. #### Cloud region ##### With Custom plan and multi-region If you have a [Custom plan](https://ckeditor.com/pricing/) with the multi-region addon, you can have multiple environments created in both regions. You can choose environment region during environment creation. ##### Without multi-region Without multi-region, all environments are created within the same region. Changing a region of an existing environment is not possible on user level, please [contact support](https://ckeditor.com/contact/). To change a region, you need to delete all environments in the old region first. Only then you can change the region and create new environments in this new region. source file: "cs/latest/developer-resources/environments/environments-multitenancy.html" ## Environments multitenancy CKEditor Cloud Services guarantee a logical separation between different environments. All secrets and keys are unique for each environment, which minimizes the chance of requesting a different environment than intended. Each environment uses separate secrets to encrypt its data in the database. You can create as many environments as you need. Removing data from one environment does not affect the state of the others. source file: "cs/latest/developer-resources/environments/overview.html" ## Environments overview **An environment** is a logical structure that separates data in the CKEditor Cloud Services ecosystem. A single product can contain many environments. It allows for creation of independent spaces on the Collaboration Server and other services. ### Security Each environment has its own set of `accessKeys` and `apiSecret`. It means that for every environment the token endpoint and the request signature script need to be separate. It reduces the risk of connecting unintentionally to other environments. Data belonging to the environment is encrypted by using different encryption keys for each environment. You can learn more about security in [System Security](#cs/latest/guides/system-security.html) guide. ### Cloud region Each environment has a [cloud region](#cs/latest/guides/saas-vs-on-premises.html--cloud-region). The region is set per-subscribtion and (unless you have Custom plan with multi-region) cannot be changed for existing environments by the user. This topic is addressed in more detail in the [Environment management](#cs/latest/developer-resources/environments/environments-management.html--cloud-region) guide. ### Next steps * [Create a new environment](#cs/latest/developer-resources/environments/environments-management.html--creating-a-new-environment) source file: "cs/latest/developer-resources/index.html" ## Cloud Services - Developer resources CKEditor Cloud Services allows to manage documents and related data by using server-server communication. It can be done in both directions. The Collaboration Server is able to communicate any other application by sending HTTP request in case of event occurrence by Webhooks. The other applications can manipulate the Collaboration Server data by using the REST API. Refer to the following sections for more information: * [Overview](#cs/latest/developer-resources/overview.html) – An introduction to Cloud Services developer resources. * [Restful APIs](#cs/latest/developer-resources/apis/overview.html) – Instructions on how to utilize the **REST API**. * [Server-side Editor API](#cs/latest/developer-resources/server-side-editor-api/editor-scripts.html) – Instructions on how to utilize the **Server-side Editor API**. * [Webhooks](#cs/latest/developer-resources/webhooks/overview.html) – Instructions on how to use the **Webhooks** mechanism. * [API Secret](#cs/latest/developer-resources/security/api-secret.html) – Instructions on how to access and use **API Secret**. * [Request Signature](#cs/latest/developer-resources/security/request-signature.html) – Instructions on how to generate **Request Signature** to authenticate requests and events. source file: "cs/latest/developer-resources/monitoring/insights-panel.html" ## Insights panel ### Insights Panel overview The Insights Panel feature allows to gather, list, and filter business (audit) logs that come from the server. These logs provide a good insight into what is going on internally in your environments. Also, the Insights Panel serves as a useful debugging tool, especially during integration. Using the Insights Panel feature, you can check logs for the CKEditor Collaboration Services, CKBox, Import/Export to Word, and Export to PDF. For Import/Export to Word and for Export to PDF, the Insights Panel feature is available only for the SaaS variant. ### Activating Insights feature By default, the Insights feature is turned off for all environments. To activate the Insights feature: 1. Log in to the [Cloud Services Portal](https://portal.ckeditor.com/) (Management Panel when using On-Premises). Go to the `Feature configuration` section of the `Cloud environments` tab for the selected environment. Note that the feature is activated separately for every environment. 2. Under the `INSIGHTS` section, switch on the `Business logs` toggle. 1. After turning on the toggle, the CS server will start collecting business and details logs for that environment from now on. ### Listing logs To list the collected logs, go to the `Insights Panel` tab in the Cloud Services Portal (or the Management Panel if using On-Premises). By default, the log list displays business and details logs from the last 2 hours. To change the time range, use the date picker to set the desired filtering range. ### Filtering logs To search the logs, use the filters to narrow down the desired results. The currently available filters are: * `Trace ID` – filtering by `traceId`. * `User ID` – filtering by `userId`, * `Error level` – filtering by `warn`, `fail` and `error` levels, * `Document ID` – filtering by `documentId` – only for the CKEditor Collaboration Services, * `Asset ID` – filtering by `assetId` – only for the CKBox, * `Category ID` – filtering by `categoryId` – only for the CKBox, Filters can be combined with one another. You can paste the filter value into the filter box (useful for searching by `trace`). It is also possible to choose one of the suggested values - these are aggregated from the logs matching your queries. #### Log structure Here are the key fields included in a log: * `msg` \- A detailed log message. * `traceId` \- A unique RPC call ID. * `data` \- An object containing additional information. It may vary between different logs. * `time` \- An occurrence date. * `tags` \- Tags associated with the log. The possible tags are `business` (information about the main action initiated by the user or an API) or `details` (detailed information about the main action or information about additional events that have been triggered by the main action). An example log: ``` { "msg": "The user authenticated to the server (userId: xhb1jjs10uq8un9wt0vc)", "traceId": "b36f1967-7778-4c0d-97a2-37caba24d59e", "data": { "socketId": "vem71503nieel8ewoxjb", "environmentId": "ciy6g1w4y1a6qm146our", "userId": "xhb1jjs10uq8un9wt0vc" }, "time": "2022-04-25T09:53:01.257Z", "tags": "business" } ``` Log stream records are grouped together and can contain both `business` and `details` logs linked together by `traceId`. Log details are available after clicking on the log record. ### Log retention All logs older than 31 days are periodically removed from the environment for the SaaS variant. For the On-Premises variant, the logs are retained for 14 days. This is done automatically and doesn’t require additional configuration. source file: "cs/latest/developer-resources/overview.html" ## Developer resources overview ### Communication with the CKEditor Cloud Services CKEditor Cloud Services provides multiple ways of communication with the application it cooperates with. Cloud Services can communicate with both fronted and backend part of the application. To communicate with the browser part, it most often utilizes the CKEditor 5 plugins, which only need to pass required configuration properties to work. Another type of communication is the **server to server communication**. The CKEditor Cloud Services provides a variety of REST APIs to communicate with. In many cases the event-based communication, through webhooks, can improve overall performance of the integration. The CKEditor Cloud Services provides many webhook events, which can be used to optimize the workflow of the application. By using a combination of webhooks and REST APIs you can create a reliable and efficient integration with the CKEditor Collaboration Server. REST APIs allow to import, export and manage data stored on the CKEditor Collaboration Server. The Webhooks inform the integrated application about events that ocurr in the documents and editing sessions. source file: "cs/latest/developer-resources/security/access-key.html" ## Access key ### Access credentials overview The Access key is used for generating a token which can be used to connect to the CKEditor Collaboration Server and other services. Single environment can use many access keys in a same time. ### Rotating access credentials It is recommended to periodically change the access credentials. It can be done by creating a new one and updating its value in the application where the token is generated. After ensuring that an updated key is available for all users, the old access key can be removed. Please note that old tokens can still be in use for short period of time, so it is recommended to give users some time to update. The update of access credentials is unnoticeable for collaborating users. Instructions on how to create and delete the access key can be found below. ### Managing the access credentials The access credentials are available for every environment in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises. You can manage them by following the steps below: #### Creating access credentials From the list of environments select the one that you want to manage. Go to the “Access credentials” tab and click the “Create a new access key” button. The modal will show up and will prompt for a name for the new access key. Provide the name and click the “Save” button. The newly created access key will be present on the list of access keys. For your safety, the access key value will disappear in a few seconds. You will be able to display it again by clicking the “Show” button. Make sure to keep the access credentials in a safe place. #### Removing the access credentials From the list of access keys, select the one that you want to remove and click the red “Remove” link in the “Actions” column. The modal will show up and will prompt for a confirmation of the access key removal. Confirm by clicking the “Remove” button. source file: "cs/latest/developer-resources/security/api-secret.html" ## API secret ### API secret overview The API secret is used for authentication in the most critical parts of the system where access should be limited. For example, the API secret is used in REST APIs and webhooks mechanisms. ### API secrets management It is possible to create up to 3 API secrets, by using the “API secrets” tab in the [Customer Portal](https://portal.ckeditor.com/). It allows for an easier API secret rotation. The “API secrets” tab also provides information such as the creation and last usage dates of a given API secret. #### Create a new API secret To generate a new API secret click the “Create a new API secret” visible on the “API secrets” tab. The API secret’s value will be shown only once. Copy the newly created API secret and save it in a safe place. It will not be possible to display the full value of this API secret again. ##### Set API secret to be used to sign webhooks requests The first API secret created in the “API secrets” tab will be automatically set for signing webhooks requests. To change which API secret should be used for that, click the “Use with webhooks” button of the given API secret in the “Actions” column. The change must be confirmed or canceled. #### Remove API secret To remove the API secret click the red “Trash” icon with the “Remove” prompt in the “Actions” column next to the API secret you want to remove. This operation cannot be undone, so it requires confirmation by writing the supplied phrase in a confirmation modal and clicking the “Confirm” button. source file: "cs/latest/developer-resources/security/request-signature.html" ## Request signature ### Request signature overview The safest solution is to only use communication over HTTPS, but there may be cases when HTTPS communication between the system and CKEditor Cloud Services is not possible, and only HTTP communication can be used. Therefore, to secure both situations, CKEditor Cloud Services uses the HMAC algorithm to secure the connections between systems. ### Algorithm Each request sent from or received by CKEditor Cloud Services should have the following headers: * `X-CS-Timestamp` * `X-CS-Signature` The signature is generated using the SHA-256 algorithm using one of [API secrets](#cs/latest/developer-resources/security/api-secret.html) as a secret and based on the following data: * HTTP method – `GET`, `POST`, `PUT`, `DELETE`. * URL – The path of the URL. For `https://docs.cke-cs.com/api/v5/docs?page=1` this is just `/api/v5/docs?page=1`. * timestamp – The same value as in the `X-CS-Timestamp` header. * body – A string from the body for `POST` and `PUT` or an empty string for other methods. The above data should be combined into one string in the following way: * Convert the HTTP method name to an upper case string. * Add the URL (path name). * Add the timestamp. * Add the body converted to a string. ### Examples Check an [example of a request signature implementation in Node.js](#cs/latest/examples/security/request-signature-nodejs.html). Check an [example of a request signature implementation in ASP.NET](#cs/latest/examples/security/request-signature-dotnet.html). Check an [example of a request signature implementation in Java](#cs/latest/examples/security/request-signature-java.html). Check an [example of a request signature implementation in PHP](#cs/latest/examples/security/request-signature-php.html). Check an [example of a request signature implementation in Python 3](#cs/latest/examples/security/request-signature-python.html). source file: "cs/latest/developer-resources/security/roles.html" ## Roles and permissions ### Roles and permissions overview Because CKEditor Cloud Services authenticates users and verifies their identity, information to which parts the user has access must be provided. For this purpose, CKEditor Cloud Services uses [tokens](#cs/latest/developer-resources/security/token-endpoint.html) and a permissions mechanism. In order to simplify and facilitate providing information about permissions, CKEditor Cloud Services uses roles. Roles contain a set of permissions describing the actions that the user can and cannot do. For a more fine-grained access control, it is possible to define permissions and use these instead of roles. It is also possible to use both roles and permissions to create a custom access solution. ### Roles for Export to PDF and Export to Word There is no need to specify any roles for the PDF and Word converters. Each user that belongs to the environment with enabled converters is allowed to use them. ### Roles for Collaboration The user is authorized only to documents listed in `auth.collaboration` and may have a different role in each document. ``` { "auth": { "collaboration": { "doc-1": { "role": "reader" }, "doc-2": { "role": "commentator" }, "doc-2": { "role": "writer" } } } } ``` The list of documents to which the user is authorized should be specified by providing their IDs or [patterns](#cs/latest/developer-resources/security/roles.html--patterns) (if the user has access to a group of documents). #### Roles Each role contains a set of permissions describing the actions that the user can and cannot do. CKEditor Cloud Services provides the following 3 roles: * `reader` – A user with this role has read-only access to the document. It means that the user is NOT authorized to make any changes to the document (this includes adding comments). This role includes the following permission types: `document:read`, `comment:read`. * `commentator` – A user with this role has read-only access to the document but is authorized to comment. It means that the user is NOT allowed to modify the content of the document but is allowed to leave comments. This role includes the following permission types: `document:read`, `comment:read`, `comment:write`, and allows to perform the following actions: * creating new comment threads, * removing own comment threads, * adding comments to all existing comment threads, * removing and modifying own comments from all existing comment threads, * resolving and reopening any comment threads (in the comments archive). * `writer` – A user with this role has full access to the document. It means that the user is authorized to make any changes to the document (including adding comments). This role includes the following permission types: `document:read`, `document:write`, `comment:read`, `comment:write`, `comment:admin`, and allows to perform the following actions: * creating new comment threads, * removing all comment threads, * adding comments to all existing comment threads, * removing and modifying own comments from all existing comment threads, * resolving and reopening any comment threads (in the comments archive). **Please note:** None of the above roles allows removing or modifying single comments created by other users. To acquire such a privilege, please include the `comment:modify_all` permission. #### Permissions For more fine-grained access control, CKSource Cloud Services uses permissions instead of predefined roles. This feature is available since CKEditor 5 v29.0.0. CKEditor Cloud Services provides five permission types: * `document:read` – A user with this permission type has read-only access to the document. It means that the user is NOT authorized to make any changes to the document (including no ability to comment). * `document:write` – A user with this permission type is authorized to modify the content of the document. This permission also allows the user to implicitly resolve any comment thread in the comments archive by removing content with a comment thread marker. What is more, it allows to implicitly reopen any comment thread by performing the undo operation. * `comment:read` – A user with this permission type has read-only access to the document’s comments. It means that the user is NOT authorized to add new comments or edit the existing ones. * `comment:write` – A user with this permission type is authorized to add new comments, edit, and remove their own comments and comment threads. Moreover, it allows to resolve and reopen any comment thread in the comment archive. * `comment:admin` – A user with this permission type can remove comment threads added by any user. However, this permission does not allow removing or modifying single replies authored by others. To take advantage of this permission, users must have also the `comment:write` permission. * `comment:modify_all` – A user with this permission type can remove or modify comments added by other users. However, this permission does not allow removing whole comment threads authored by others. ``` { "auth": { "collaboration": { "*": { "permissions": [ "document:read", "document:write", "comment:read" ] } } } } ``` The described permissions authorize the user only for the actions described. For example, the `document:write` permission, which authorizes the user to modify the content of the document, does not include the `document:read` permission. It means, that the `document:read` permission should be included in the permissions list as well, to authorize the user to read the document. The `comment:admin` permission is no exception to this rule. #### Mixing roles and permissions In some cases, it would be handy to mix predefined roles with permissions. Example: The following sample defines a user that has the `commentator` role in all documents and additionally, the user has the `comment:admin` permission which allows for removing comment threads added by other users. ``` { "auth": { "collaboration": { "*": { "role": "commentator", "permissions": [ "comment:admin" ] } } } } ``` #### Patterns The patterns facilitate determining permissions to a group of documents. You can provide a pattern with wildcard characters instead of the document ID. The pattern will then cover multiple documents IDs. Example: The following sample defines a user that has the `reader` role in all documents matching the pattern: `docs-*`. Examples of matching documents could be: `docs-titlepage` and `docs-category-document`. ``` { "auth": { "collaboration": { "docs-*": { "role": "reader" } } } } ``` source file: "cs/latest/developer-resources/security/token-endpoint.html" ## Token endpoint ### Token endpoint overview To connect [CKEditor 5](https://ckeditor.com/ckeditor-5/) or [CKEditor 4](https://ckeditor.com/ckeditor-4/) with CKEditor Cloud Services, you need to create your own token endpoint. This guide will explain a few principles that you need to know to create it. ### How CKEditor Cloud Services uses tokens To authenticate users, CKEditor Cloud Services uses tokens. The purpose of tokens is to inform the cloud services that the user has access to resources and to which environment the user should connect to. The authenticity of tokens is provided by a digital signature. The token endpoint is where the client using CKEditor makes a request to get the token. It should return the token only if the user proves their identity. The token endpoint should be placed inside of your system. ### The JSON Web Tokens specification CKEditor Cloud Services tokens are represented as JSON Web Tokens (JWT). JWT is an open [standard](https://tools.ietf.org/html/rfc7519) for transmitting digitally signed information. Using it ensures that the data comes from a trusted source. The tokens consist of three parts: * header * payload * signature Each part is encoded in `base64url` and separated by periods: ``` header.payload.signature ``` An example token can look like this: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiSm9obiJ9.3MOd-0xmppWAX_86vMPjQ0PTKAniCtr762UTM5WuGa8 ``` #### Header The header indicates that it is a JWT and defines the hashing algorithm for calculating the signature. It is simply a JSON object with the `typ` and `alg` properties, where `typ` always contains the `JWT` value and `alg` contains the name of the algorithm used. #### Payload The payload is a JSON object with the claims. In CKEditor Cloud Services it is mostly used for specifying the environment, user data and permissions. The JWT standard specifies some predefined claims like `aud` (audience), `exp` (expiration time), `sub` (subject) or `iat` (issued at). In CKEditor Cloud Services we use `aud` for identifying environments, `iat` for checking if the token has not expired and `sub` for identifying users. #### Signature Unlike the header and the payload, the signature is not a JSON object but instead raw bytes produced by the cryptographic algorithm. The signature is calculated for the header merged with the payload. It enables the authentication of data and verification that the token has not been tampered with: ``` signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) ``` CKEditor Cloud Services supports signatures created by the `HS256`, `HS384` and `HS512` algorithms. ### Token The token for the CKEditor Cloud Services should specify the `Environment ID` as the `aud` property and it needs to be signed with the `Access key`. Both can be found in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises - refer to the [documentation for environments management](#cs/latest/developer-resources/environments/environments-management.html). Optionally, the token can contain the user data. Otherwise CKEditor Cloud Services will treat the user as an anonymous user. #### User If the user is not anonymous, the payload should contain the `sub` claim, which identifies the user. You can also put a `user` object to define other properties like `name`, `email` or `avatar`. In the case of an anonymous user, a random user ID will be generated. #### Billings While using CKEditor Cloud Services in the SaaS variant, **you will be billed for every unique user**. Customers who use the On-Premises variant of the CKEditor Collaboration Server are limited by the number of unique connected users. A unique user is counted based on a unique pair of the `aud`(environment ID) and `sub`(user ID) properties from the token. In the case of an anonymous user, every user will be billed separately as a random user ID will be assigned. #### Token payload ##### Payload properties The following properties **must** be included in the payload: * `aud` – The environment ID. * `iat` – “Issued at”. Make sure that `iat` is present and contains a correct time stated in seconds. Some JWT implementations do not include it by default. Sometimes the system time may also be invalid, causing weird issues with tokens (see e.g. [Docker for Mac time drift](https://github.com/docker/for-mac/issues/2076)). * `auth` – Permissions for the services being used. If you plan to use collaboration, the `auth.collaboration` roles are required. If you plan to use CKEditor AI, the `auth.ai.permissions` array is required. If you just use Export to Word/PDF, you may skip it. The properties that are optional: * `sub` – The user ID. * `user` – User information. Providing `name` and `email` is recommended. * `exp` – Token expiration time. Identifies the expiration time after which the JWT will not be accepted. Cloud Services only accept tokens no older than 24 hours. This field can be used to shorten the token validity time. ##### Roles and permissions To define permissions to resources, the token payload should include the `auth` object with specified user roles and/or permissions. ``` { "auth": { "collaboration": { "doc-1": { "role": "writer" }, "doc-2": { "role": "commentator", "permissions": [ "comment:admin" ] }, "doc-3": { "permissions": [ "document:read", "document:write", "comment:read" ] } } } } ``` ##### AI permissions To control access to CKEditor AI features, the token payload should include the `auth.ai.permissions` array. Each permission follows the format `ai::`. ``` { "auth": { "ai": { "permissions": [ "ai:conversations:*", "ai:models:agent", "ai:actions:system:*", "ai:reviews:system:*" ] } } } ``` ##### Example token payload for collaboration The example below presents a complete token payload with access to editing all documents in the environment: ``` { "aud": "NQoFK1NLVelFWOBQtQ8A", "iat": 1511963669, "sub": "exampleuser", "user": { "email": "example@cksource.com", "name": "A User", "avatar": "http://example.com/avatars/john.png" }, "auth": { "collaboration": { "*": { "role": "writer" } } } } ``` ##### Example token payload for AI The example below presents a token payload granting access to AI conversations, actions, and reviews with the recommended `agent` model: ``` { "aud": "NQoFK1NLVelFWOBQtQ8A", "iat": 1511963669, "sub": "exampleuser", "auth": { "ai": { "permissions": [ "ai:conversations:*", "ai:models:agent", "ai:actions:system:*", "ai:reviews:system:*" ] } } } ``` ##### Example token payload for collaboration and AI When using both collaboration and AI features together, include both `auth.collaboration` and `auth.ai` in the same token: ``` { "aud": "NQoFK1NLVelFWOBQtQ8A", "iat": 1511963669, "sub": "exampleuser", "user": { "email": "example@cksource.com", "name": "A User" }, "auth": { "collaboration": { "*": { "role": "writer" } }, "ai": { "permissions": [ "ai:conversations:*", "ai:models:agent", "ai:actions:system:*", "ai:reviews:system:*" ] } } } ``` ### Requests to the token endpoint The token for CKEditor Cloud Services is requested by the CKEditor 5 [real-time collaborative editing](https://ckeditor.com/collaboration/real-time-collaborative-editing/). #### Simple usage ##### CKEditor 5 The easiest way to request the token endpoint is to set the [config.cloudServices.tokenUrl](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-tokenUrl) option to the endpoint. It will be requested from the editor by the [Cloud Services plugin](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservices-CloudServices.html) with a simple HTTP request at the initialization stage and then after every hour. ``` ClassicEditor.create( element, { // ... cloudServices: { tokenUrl: 'https://example.com/cs-token-endpoint' } } ); ``` If you need more control over the request, refer to the [Customizing the token request method](#cs/latest/developer-resources/security/token-endpoint.html--customizing-the-token-request-method) section below. ##### CKEditor 4 The easiest way to request the token endpoint is to set the [cloudServices\_tokenUrl](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR%5Fconfig.html#cfg-cloudServices%5FtokenUrl) option to the endpoint. It will be requested from the editor with a simple HTTP request at the initialization stage and then again every hour. ``` CKEDITOR.replace( 'editor', { cloudServices_tokenUrl: 'https://example.com/cs-token-endpoint', } ); ``` You can find more information about the integration with CKEditor 4 in the [Easy Image Integration guide](https://ckeditor.com/docs/ckeditor4/latest/guide/dev%5Feasyimage%5Fintegration.html). #### Customizing the token request method ##### CKEditor 5 If you would like to change the default method of requesting the token from the server (described in the previous section), you can set the [config.cloudServices.tokenUrl](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-tokenUrl) option to a callback. This callback will be used by the [Cloud Services plugin](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservices-CloudServices.html) to retrieve the token. It allows specifying exact HTTP request parameters for the token endpoint that should return the token value. The callback needs to return a promise that will be resolved with the token value or rejected with an error. An example below presents setting a custom callback to the `tokenUrl` configuration that adds a custom header to the request: ``` const tokenUrl = 'https://example.com/cs-token-endpoint'; ClassicEditor.create( element, { // ... cloudServices: { tokenUrl: () => { return new Promise( ( resolve, reject ) => { const xhr = new XMLHttpRequest(); xhr.open( 'GET', tokenUrl ); xhr.addEventListener( 'load', () => { const statusCode = xhr.status; const xhrResponse = xhr.response; if ( statusCode < 200 || statusCode > 299 ) { return reject( new Error( 'Cannot download a new token!' ) ); } return resolve( xhrResponse ); } ); xhr.addEventListener( 'error', () => reject( new Error( 'Network error' ) ) ); xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) ); xhr.setRequestHeader( customHeader, customValue ); xhr.send(); } ); } } } ); ``` The current token value is accessible via the `CloudServices` plugin. ``` const cloudServices = editor.plugins.get( 'CloudServices' ); const tokenValue = cloudServices.token.value; ``` ### Responses from the token endpoint The endpoint should respond with a generated token in the form of a string. An example response might look like this: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiSm9obiJ9.3MOd-0xmppWAX_86vMPjQ0PTKAniCtr762UTM5WuGa8 ``` ### Tools We highly recommend using the libraries listed on [jwt.io](https://jwt.io/#libraries-io) to create the token. You can also check our token endpoint implementation examples in the following languages: * [ASP.NET](#cs/latest/examples/token-endpoints/dotnet.html), * [Java](#cs/latest/examples/token-endpoints/java.html), * [Node.js](#cs/latest/examples/token-endpoints/nodejs.html), * [PHP](#cs/latest/examples/token-endpoints/php.html), * [Python](#cs/latest/examples/token-endpoints/python.html), If you encounter problems creating the token endpoint in another server-side language, or if you have any other suggestions regarding the documentation, please [contact us](https://ckeditor.com/contact/). ### Most common issues #### Invalid token ``` CKEditorError: cloud-services-internal-error: Invalid token. Error Trace id: . Read more: {"errors":[]} ``` Check if the token was created in accordance with our [standards](#cs/latest/developer-resources/security/token-endpoint.html--responses-from-the-token-endpoint). You can also refer to our [token endpoint examples](#cs/latest/examples/token-endpoints/nodejs.html). Please also ensure that you are using a correct and valid [access key](#cs/latest/developer-resources/security/access-key.html--creating-access-credentials) which is assigned to the right environment. You can validate the token using the [jwt.io debugger](https://jwt.io/#encoded-jwt). #### You don’t have enough permissions to access this resource ``` CKEditorError: cloud-services-internal-error: You don't have enough permissions to access this resource. Error Trace id: . Read more: {"errors":[]} ``` Please ensure that your token endpoint includes the right roles for the requested resource. You can read more about the role mechanism in the [Roles guide](#cs/latest/developer-resources/security/roles.html). source file: "cs/latest/developer-resources/server-side-editor-api/editor-scripts.html" ## Editor scripts The Server-side Editor API can be accessed through the `POST /collaborations/{document_id}/evaluate-script` endpoint from the [REST API](https://help.cke-cs.com/api/docs). This endpoint allows you to execute JavaScript code that uses the CKEditor–5 API directly on the Cloud Services server. ### Prerequisites * An [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) needs to be uploaded. * The source document needs to be created after uploading the editor bundle. * The source document needs to have an active collaboration session. You can initialize the document session programatically via [import collaboration session content directly from storage](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations/post) REST endpoint. ### Request Structure The request body should contain a `script` parameter with the JavaScript code to be executed: ``` { "script": "// Your JavaScript code here that uses the CKEditor 5 API" } ``` The `script` parameter should contain valid JavaScript code that can access the CKEditor–5 API. The script has access to: * **Editor instance**: The CKEditor–5 editor instance is available in the script context * **Document model**: Access to the document’s data model for content manipulation * **Editor plugins**: All loaded plugins and their APIs * **Collaboration features**: Access to comments, suggestions, and revision history It should be a properly formatted string according to `ECMA-404 (The JSON Data Interchange Standard)`. The only characters you must escape are `\` and `"`. Control codes such as `\n`, `\t`, etc., can be removed or escaped as well. You can also include a `user` object in the request body to control the user details, like name, during script execution. The `hidden_in_presence_list` property allows you to hide the user from the presence list in the collaboration session, which is useful for server-side script executions that should not appear as active users. The example below shows a basic script for getting editor data, along with user context configuration that allows you to execute scripts with specific user context and to control how the user appears in the collaboration session: ``` { "script": "return editor.getData()", "user": { "id": "user-123", "name": "John Doe", "hidden_in_presence_list": true } } ``` You can specify a `plugins_to_remove` parameter in the request body to provide a list of plugin names for removal from the editor before executing the script. It is useful when you need to disable certain editor features or plugins that might interfere with your script execution. Note that, it is different from the `removePlugins` option that you may have specified in the editor bundle configuration during the bundle upload, as it permanently excludes those plugins from typical document conversions. The `removePlugins` option is often used to exclude plugins that communicate with the internet. However, in the case of the Server-side Editor API, such plugins can be included in your editor bundle and used in scripts if needed. ``` { "script": "return editor.getData()", "plugins_to_remove": ["DocumentOutline", "ExportInlineStyles"] } ``` #### Syntax Scripts can be written in both async and non-async ways. You can use `await` for asynchronous operations: ``` // Non-async script editor.getData(); // Async script await Promise.resolve( editor.getData() ); ``` You can return data from your script using the `return` statement. The returned value will be available as literal data or as a JSON (in case of an object) in the API response: ``` // Return processed data const content = editor.getData(); return { content: content, wordCount: content.split(' ').length }; ``` The CKEditor 5 editor instance is available globally as the `editor` object. You can access all editor methods and properties: ``` // Get editor content const content = editor.getData(); // Set editor content editor.setData('

New content

', { suppressErrorInCollaboration: true } ); // Access editor model const model = editor.model; // Use editor commands editor.execute('bold'); ``` Scripts have access to most browser and JavaScript APIs, with some restrictions for security reasons. Refer to the [security considerations](#cs/latest/developer-resources/server-side-editor-api/security.html) guide for detailed implementation.
#### Authentication All requests to the Server-side Editor API require proper authentication using: * `X-CS-Timestamp` header with the current timestamp * `X-CS-Signature` header with a valid request signature Refer to the [request signature](#cs/latest/developer-resources/security/request-signature.html) guide for detailed implementation. #### Response The API returns a `201` status code on successful execution. The response contains the data returned by your script under the `data` attribute: ``` { "data": "

Editor content

" } ``` For objects returned by your script, the response will be: ``` { "data": { "content": "

Editor content

", "wordCount": 5 } } ```
#### Error handling When a script fails to execute, the API returns detailed error information. Example error response: ``` { "message": "Editor script failed to evaluate", "trace_id": "2daa364a-a658-4123-9630-50497447ed07", "status_code": 400, "data": { "error": { "name": "TypeError", "message": "Cannot read properties of undefined (reading 'get')", "details":[ "Cannot read properties of undefined (reading 'change')" ] } }, "level": "trace", "explanation": "The provided script cannot be evaluated due to the attached error", "action": "Please check the error and script syntax or contact support if the problem persists" } ``` The `data.error.details` field contains an array of intercepted and parsed browser console errors, which can help you identify the root cause of script execution failures. #### Debugging and Testing When developing and testing your editor scripts, you can enable the debug mode by setting the `debug: true` parameter in your API request. This will allow you to collect the `console.debug` logs from the script execution. It can be helpful for troubleshooting and understanding script behavior. ``` { "script": "console.debug('Processing editor data', { wordCount: editor.getData().length }); return editor.getData();", "debug": true } ``` With debug mode enabled, any `console.debug()` calls in your script will be captured and included in the response. This provides additional insight into your script’s execution flow: ``` { "data": "

Editor content

", "metadata": { "logs": [ { "type": "debug", "createdAt": "2025-08-08T12:00:00.000Z", "data": [ "Processing editor data", { "wordCount": 245 } ] } ] } } ``` In order to improve the debugging experience in case of an error tracking process, the evaluation error also includes the collected `logs` during the script evaluation. This means that if a script fails to execute when debug mode is enabled, you will receive both the error details and any debug logs that were captured before the failure occurred, providing valuable context for troubleshooting script issues. ``` { "message": "Editor script failed to evaluate", "trace_id": "2daa364a-a658-4123-9630-50497447ed07", "status_code": 400, "data": { "error": { "name": "TypeError", "message": "Cannot read properties of undefined (reading 'get')", }, "logs": [ { "createdAt": "2025-08-08T12:00:00.000Z", "type": "debug", "data": [ "Processing editor data", { "wordCount": 245 } ] } ] }, "explanation": "The provided script cannot be evaluated due to the attached error", "action": "Please check the error and script syntax or contact support if the problem persists" } ``` Please remember, that only serializable objects can be outputted in debug logs. Custom error attributes and non-serializable content will be omitted from the response.
### Next steps * [Server-side Editor API security considerations](#cs/latest/developer-resources/server-side-editor-api/security.html) source file: "cs/latest/developer-resources/server-side-editor-api/security.html" ## Server-side Editor API security considerations The Server-side Editor API includes security measures to prevent malicious code execution. All scripts are analyzed before the execution to detect and block potentially dangerous operations. ### Restricted Operations The following categories of operations are blocked for security reasons: #### Global Functions and APIs The API blocks access to potentially dangerous global functions and APIs that could be used for malicious purposes: * **Code execution functions**: `eval`, `Function`, `importScripts` * **Timing functions**: `setTimeout`, `setInterval`, `requestAnimationFrame` * **Network and communication**: `XMLHttpRequest`, `WebSocket`, `Worker`, `SharedWorker`, `BroadcastChannel` * **File and data access**: `FileReader`, `indexedDB` * **Encoding/decoding**: `atob`, `btoa`, `decodeURIComponent` * **Storage access**: `localStorage`, `sessionStorage` * **Browser navigation**: `history`, `import` * **Performance monitoring**: `performance` #### Browser APIs and Document Manipulation Access to browser-specific APIs is restricted to prevent unauthorized document manipulation and browser state changes: * **Document creation and modification**: `document.createElement`, `document.write`, `document.writeln`, `document.createElementNS` * **Document properties**: `document.cookie`, `document.body`, `document.head`, `document.location` * **Window operations**: `window.open`, `window.location`, `window.postMessage`, `window.btoa`, `window.atob`, `window.document`, `window.navigator`, `window.performance` * **URL manipulation**: `URL.createObjectURL` * **Browser information**: `navigator.sendBeacon`, `navigator.userAgent`, `navigator.platform` * **Performance data**: `performance.now`, `performance.getEntries` * **History manipulation**: `history.pushState`, `history.replaceState` #### Element Operations Direct manipulation of DOM elements is blocked to prevent injection attacks: * **HTML injection**: `element.innerHTML`, `element.outerHTML`, `element.insertAdjacentHTML` * **Event handling**: `element.addEventListener` #### Constructor Calls Creating certain objects is restricted to prevent potential security vulnerabilities: * **File objects**: `new Blob()`, `new File()`, `new FileReader()` * **Communication objects**: `new WebSocket()`, `new Worker()`, `new SharedWorker()`, `new BroadcastChannel()` * **Code execution**: `new Function()` * **URL objects**: `new URL()` #### URL Schemes Certain URL schemes are blocked to prevent code injection and data leakage: * **Data URLs**: `data:` * **Blob URLs**: `blob:` * **JavaScript URLs**: `javascript:` * **VBScript URLs**: `vbscript:` ### Security Violations If your script contains restricted operations, the API will return an error with details about the violation, including the line and column position. ### Next steps * [Server-side Editor API Node.js example](#cs/latest/examples/server-side-editor-api-examples/extract-document-content-in-nodejs.html) source file: "cs/latest/developer-resources/webhooks/events.html" ## Events Each event corresponds with a certain action that can happen in CKEditor Cloud Services in a certain environment. Also, each event has a specific payload format with the relevant event information. Before you use Webhooks, please get familiar with the basic principals of the feature. You can read more about how to use Webhooks in [the Overview article](#cs/latest/developer-resources/webhooks/overview.html). ### Collaboration The following events can be triggered for the Collaboration service. #### User connected Name: `collaboration.user.connected` (previously `document.user.connected`) Description: Triggered when a user joins a collaboration session. ##### Payload * `document.id` – The ID of the document that the user connected to. * `user.id` – The ID of the user. * `connected_users` – The list of currently connected users. ##### Example The following example presents a webhook request sent after a user connected to the document. ``` { "event": "collaboration.user.connected", "environment_id": "environment-1", "payload": { "user": { "id": "user-1" }, "document": { "id": "document-1" }, "connected_users": [ { "id": "user-2" } ] }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### User disconnected Name: `collaboration.user.disconnected` (previously `document.user.disconnected`) Description: Triggered when a user disconnects from an active collaboration session. ##### Payload * `document.id` – The ID of the document that the user disconnected from. * `user.id` – The ID of the user. * `connected_users` – The list of currently connected users. ##### Example The following example presents a webhook request sent after a user disconnected from the document. ``` { "event": "collaboration.user.disconnected", "environment_id": "environment-1", "payload": { "user": { "id": "user-1" }, "document": { "id": "document-1" }, "connected_users": [ { "id": "user-2" } ] }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Collaboration session updated Name: `collaboration.document.updated` Description: Triggered every 10 minutes or 5000 versions when the content of the collaboration session is being updated. The event will also be emitted when the last user disconnects from a collaboration session. ##### Payload * `document.id` – The ID of the updated document. * `document.updated_at` – The date of the document update. * `document.updated_by` – The ID of the last user performing the update. * `document.version` – The version of the updated document. ##### Example The following example presents a webhook request sent after a document is updated. ``` { "event": "collaboration.document.updated", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "updated_at": "2019-05-29T08:17:56.761Z", "updated_by": "user-1", "version": 2000 } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Collaboration session update exported Name: `collaboration.document.update.exported` Description: Triggered every 10 minutes or 5000 versions when the content of the collaboration session is being updated. The webhook is also triggered when the collaboration session ends and when the last user disconnects. It contains a document content session. All the following conditions have to be met: 1. The [editorBundle](#cs/latest/guides/collaboration/editor-bundle.html) is uploaded. 2. The document storage feature is disabled. ##### Payload * `document.id` – The ID of the updated document. * `document.updated_at` – The date of the document update. * `document.version` – The version of the updated document. * `document.status` – The status of an export. * `document.data` – The document content. The property can be empty if exporting a collaboration session failed. * `document.attributes` – The document attributes object. The property may be sent when a document has been exported from a multi-root editor. ##### Example The following example presents a webhook request sent after a document is updated. ``` { "event": "collaboration.document.update.exported", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "exported_at": "2019-05-29T08:17:56.761Z", "version": 2000, "status": "success", "data": "

Document content

", "attributes": { "foo": "bar" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ```
#### Collaboration session finished Name: `collaboration.document.finished` Description: Triggered when a collaboration session for a document has ended and the [temporary data of the document](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data) has been removed. The webhook is triggered regardless of the document storage configuration. To receive the exported document data, use the `collaboration.document.exported` webhook. ##### Payload * `document.id` – The ID of the removed document. * `document.removed_at` – The date of the document removal. ##### Example The following example presents a webhook request sent after a collaboration session for a document has ended: ``` { "event": "collaboration.document.finished", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "removed_at": "2019-05-29T08:17:56.761Z" } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Collaboration session exported Name: `collaboration.document.exported` Description: Triggered whenever all of the following conditions are met: 1. A collaboration session has ended. 2. The document data has been successfully exported. 3. The [editorBundle](#cs/latest/guides/collaboration/editor-bundle.html) is uploaded. 4. The document storage is disabled. ##### Payload * `document.id` – The ID of the removed document. * `document.data` – The data of the removed document. * `document.removed_at` – The date when the document has been removed. * `document.version` – The version of the removed document. * `document.attributes` – The document attributes object. The property may be sent when a document has been exported from a multi-root editor. ##### Example The following example presents a webhook request sent after a collaboration session has ended and the document data has been successfully exported: ``` { "event": "collaboration.document.exported", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "data": "

Document content

", "removed_at": "2019-05-29T08:16:12.755Z", "version": 10, "attributes": { "foo": "bar" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ```
#### Collaboration session export failed Name: `collaboration.document.export.failed` Description: Triggered whenever both of the following conditions are met: (1) the system cannot convert document operations after a collaboration session has ended, and (2) the document storage is disabled. ##### Payload * `document.id` – The ID of the removed document. * `document.reason` – The reason of the failed document operations conversion. * `document.removed_at` – The date when the document has been removed. * `document.version` – The version of the removed document. ##### Example The following example presents a webhook request sent after a collaboration session for a document has ended, but some of its operations could not be applied: ``` { "event": "collaboration.document.export.failed", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "reason": "general-conversion-fail", "removed_at": "2019-05-29T08:16:12.755Z", "version": 10 } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Collaboration session recovered Name: `collaboration.document.recovered` Description: Triggered when a collaboration session for a document has expired and the [temporary data of the document](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data) have been removed but some of the document operations could not be applied. The webhook is sent only when the [document storage feature](#cs/latest/guides/collaboration/document-storage.html) is disabled, and it will contain only correctly applied operations. ##### Payload * `document.id` – The ID of the removed document. * `document.removed_at` – The date of the document removal. * `document.data` – The data of the removed document including only the successfully applied operations. * `document.version` – The version of the removed document. * `document.attributes` – The document attributes object. The property may be sent when a document has been exported from a multi-root editor. ##### Example The following example presents a webhook request sent after a document has been removed but some of its operations could not be applied. ``` { "event": "collaboration.document.recovered", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "removed_at": "2019-05-29T08:17:56.761Z", "data": "

Document content

", "version": 10, "attributes": { "foo": "bar" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ```
### Document storage #### Document saved Name: `storage.document.saved` Description: Triggered when the document data is saved. ##### Payload * `document.id` – The ID of the saved document. * `document.saved_at` – The date of the document save. * `document.download_url` – The URL to download the document. * `document.attributes` – The document attributes object. The property may be sent when a document has been exported from a multi-root editor. ##### Example The following example presents a webhook request sent after a document is saved. ``` { "event": "storage.document.saved", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "saved_at": "2019-05-29T08:17:56.761Z", "download_url": "/api/v5/environment-1/storage/document-1", "attributes": { "foo": "bar" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Document save failed Name: `storage.document.save.failed` Description: Triggered when a document data save has failed. It may happen when a different editor bundle or its configuration is used on your website and the CKEditor Cloud Services server under the same [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion). Refer to the [Editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) guide for more information. ##### Payload * `document.id` – The ID of the document. * `document.failed_at` – The date of the document save failure. * `editor.bundleVersion` – The `bundleVersion` of the editor used during the save. * `fail.reason` – The reason of the document save failure. * `fail.details` – The details of the document save failure. * `fail.trace_id` – The trace ID of the document save failure. ##### Example The following example presents a webhook request sent after a document data save has failed. ``` { "event": "storage.document.save.failed", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "failed_at": "2019-05-29T08:17:56.761Z" }, "editor": { "bundleVersion": "some_unique_bundle_version" }, "fail": { "reason": "Error while processing document.", "details": "model-position-fromjson-no-root: Cannot create position for document.", "trace_id": "trace-id-1" } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Document removed Name: `storage.document.removed` Description: Triggered when the document data is removed from the storage. The document can be removed using a REST API call (see the [Storage](https://help.cke-cs.com/api/docs#tag/Storage) REST API section) or when the document storage feature is being turned off. After you disable the feature in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises, all stored documents are removed. ##### Payload * `document.id` – The ID of the document. * `document.removed_at` – The date of the document removal. * `document.data` – The data of the removed document. * `document.attributes` – The document attributes object. The property may be sent when a document has been exported from a multi-root editor. ##### Example The following example presents a webhook request sent after a document is removed from the storage. ``` { "event": "storage.document.removed", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "removed_at": "2019-05-29T08:17:56.761Z", "data": "

Document content

", "attributes": { "foo": "bar" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ```
### Comments The following events can be triggered for the Comments service. #### Comment added Name: `comment.added` Description: Triggered when a comment is created. ##### Payload * `document.id` – The ID of the document in which the comment was added. * `comment.id` – The ID of the added comment. * `comment.created_at` – The creation date of the comment. * `comment.content` – The content of the added comment. * `comment.attributes` – The attributes of the added comment. * `comment.thread_id` – The thread ID that the comment was added to. * `comment.user.id` – The ID of the author of the comment. * `comment.thread` – The optional field sent with data regarding comment archive feature. * `comment_thread.id` – The ID of the added comment thread. * `comment.thread.created_at` – The date of the comment thread creation. * `comment.thread.context` – The context of the comment thread, required for the comments archive feature. * `comment.thread.attributes` – The attributes of the added comment thread. ##### Example The following example presents a webhook request sent after a comment is added. ``` { "event": "comment.added", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment": { "id": "comment-1", "created_at": "2019-05-29T08:17:53.450Z", "content": "Some comment content.", "attributes": { "foo": "bar" }, "thread_id": "thread-1", "user": { "id": "user-1" }, "thread": { "id": "thread-1", "created_at": "2019-05-29T08:17:53.450Z", "context": { "type": "value" }, "attributes": { "attribute": "value" } } } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment updated Name: `comment.updated` Description: Triggered when a comment is updated. ##### Payload * `document.id` – The ID of the document in which the comment was updated. * `comment.id` – The ID of the updated comment. * `comment.updated_at` – The date of the comment update. * `comment.content` – The content of the updated comment. * `comment.old_content` – The old content of the updated comment. * `comment.attributes` – The attributes of the updated comment. * `comment.old_attributes` – The old attributes of the updated comment. * `comment.thread_id` – The thread ID in which the comment was updated. * `comment.user.id` – The ID of the author of the updated comment. * `comment.thread` – The optional field sent with data regarding comment archive feature. * `comment_thread.id` – The ID of the added comment thread. * `comment.thread.created_at` – The date of the comment thread creation. * `comment.thread.context` – The context of the comment thread, required for the comments archive feature. * `comment.thread.resolved_at` – The date of the comment thread resolve. Sent only when a comment thread is resolved. * `comment.thread_resolved_by` – The id of the user who resolved the comment thread. Sent only when a comment thread is resolved. * `comment.thread.attributes` – The attributes of the added comment thread. ##### Example The following example presents a webhook request sent after a comment is updated. ``` { "event": "comment.updated", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment": { "id": "comment-1", "updated_at": "2019-05-29T08:17:53.450Z", "content": "Some comment content.", "old_content": "Some old comment content.", "attributes": { "foo": "new" }, "old_attributes": { "foo": "old" }, "thread_id": "thread-1", "user": { "id": "user-1" }, "thread": { "id": "thread-1", "created_at": "2019-05-29T08:17:53.450Z", "context": { "type": "value" }, "resolved_at": "2019-05-29T08:17:53.450Z", "resolved_by": "user-1", "attributes": { "attribute": "value" } } } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment removed Name: `comment.removed` Description: Triggered when a comment is removed. ##### Payload * `document.id` – The ID of the document from which the comment was removed. * `comment.id` – The ID of the removed comment. * `comment.removed_at` – The date of the comment removal. * `comment.user.id` – The ID of the user who removed the comment. ##### Example The following example presents a webhook request sent after a comment is removed. ``` { "event": "comment.removed", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment": { "id": "comment-1", "removed_at": "2019-05-29T08:17:53.450Z", "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread added Name: `commentthread.added` Description: Triggered when a comment thread is added. ##### Payload * `document.id` – The ID of the document from which the comment thread was added. * `comment_thread.id` – The ID of the added comment thread. * `comment_thread.comments` – The list of comments from the added comment thread. * `comment_thread.created_at` – The date of the comment thread creation. * `comment_thread.context` – The context of the comment thread, required for the comments archive feature. * `comment_thread.attributes` – The attributes of the added comment thread. ##### Example The following example presents a webhook request sent after a comment thread is added. ``` { "event": "commentthread.added", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "comments": [ { "id": "comment-1", "attributes": { "foo": "bar" }, "content": "Some comment content", "user_id": "user-1", "created_at": "2019-05-29T08:17:53.450Z" } ], "created_at": "2019-05-29T08:17:53.450Z", "context": { "type": "value" }, "attributes": { "attribute": "value" } } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread updated Name: `commentthread.updated` Description: Triggered when a comment thread is updated. ##### Payload * `document.id` – The ID of the document from which the comment thread was updated. * `comment_thread.id` – The ID of the updated comment thread. * `comment_thread.comments` – The list of comments IDs from the updated comment thread. * `comment_thread.updated_at` – The date of the comment thread update. * `comment_thread.updated_by` – The ID of the user who updated the comment thread. * `comment_thread.context` – The updated context of the comment thread, required for the comments archive feature. * `comment_thread.attributes` – The updated attributes of the comment thread. * `comment_thread.unlinked_at` – The date the comment has been unlinked, meaning that the text it was assigned to is not present in the document anymore. ##### Example The following example presents a webhook request sent after a comment thread is updated. ``` { "event": "commentthread.updated", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "comments": [ { "id": "comment-1" } ], "updated_at": "2019-05-29T08:17:53.450Z", "updated_by": "user-1", "context": { "type": "value" }, "attributes": { "attribute": "value" }, "unlinked_at": "2019-05-29T08:17:53.450Z" } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread resolved Name: `commentthread.resolved` Description: Triggered whenever a comment thread is resolved. ##### Payload * `document.id` – The ID of the document from which the comment thread was resolved. * `comment_thread.id` – The ID of the resolved comment thread. * `comment_thread.comments` – The list of comment IDs from the resolved comment thread. * `comment_thread.resolved_at` – The date when the comment thread was resolved. * `comment_thread.resolved_by` – The id of the user who resolved the comment thread. ##### Example The following example presents a webhook request sent after a comment thread is resolved. ``` { "event": "commentthread.resolved", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "comments": [ { "id": "comment-1" } ], "resolved_at": "2019-05-29T08:17:53.450Z", "resolved_by": "user-1" } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread reopened Name: `commentthread.reopened` Description: Triggered whenever a comment thread is reopened. ##### Payload * `document.id` – The ID of the document from which the comment thread was reopened. * `comment_thread.id` – The ID of the reopened comment thread. * `comment_thread.comments` – The list of comment IDs from the reopened comment thread. * `comment_thread.reopened_at` – The date whe the comment thread was reopened. * `comment_thread.reopened_by` – The ID of the user who reopened the comment thread. ##### Example The following example presents a webhook request sent after a comment thread is reopen. ``` { "event": "commentthread.reopened", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "comments": [ { "id": "comment-1" } ], "reopened_at": "2019-05-29T08:17:53.450Z", "reopened_by": "user-1" } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread removed Name: `commentthread.removed` Description: Triggered when a comment thread is removed. ##### Payload * `document.id` – The ID of the document from which the comment thread was removed. * `comment_thread.id` – The ID of the removed comment thread. * `comment_thread.removed_at` – The date of the comment thread removal. * `comment_thread.removed_by` – The ID of the user who removed the comment thread. * `comment_thread.comments` – The list of comments from the removed comment thread. ##### Example The following example presents a webhook request sent after a comment thread is removed. ``` { "event": "commentthread.removed", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "removed_at": "2019-05-29T08:17:53.450Z", "removed_by": "user-1", "comments": [ { "id": "comment-1" } ] } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment thread restored Name: `commentthread.restored` Description: Triggered when a comment thread is restored. A comment thread can be removed by removing the text in a document. The undo operation can restore the removed text but also restore the comment thread. ##### Payload * `document.id` – The ID of the document where the comment thread was restored. * `comment_thread.id` – The ID of the restored comment thread. * `comment_thread.restored_at` – The date of the comment thread restoration. * `comment_thread.restored_by` – The ID of the user who restored the comment thread. * `comment_thread.comments` – The list of comments from the restored comment thread. ##### Example The following example presents a webhook request sent after a comment thread is restored. ``` { "event": "commentthread.restored", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_thread": { "id": "comment-thread-1", "restored_at": "2019-05-29T08:17:53.450Z", "restored_by": "user-1", "comments": [ { "id": "comment-1" } ] } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Comment threads removed Name: `commentthread.all.removed` Description: Triggered when all comment threads in a document are removed. ##### Payload * `document.id` – The ID of the document from which the comment threads were removed. * `comment_threads` – The list of removed comment threads. * `comment_threads[].id` – The ID of the removed comment thread. * `comment_thread[].removed_at` – The date of the comment thread removal. * `comment_thread[].comments` – The list of comments from the removed comment threads. ##### Example The following example presents a webhook request sent after all comment threads are removed from a document. ``` { "event": "commentthread.all.removed", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment_threads": [ { "id": "comment-thread-1", "removed_at": "2019-05-29T08:17:53.450Z", "comments": [ { "id": "comment-1" } ] }, { "id": "comment-thread-2", "removed_at": "2019-05-29T08:17:53.450Z", "comments": [ { "id": "comment-2" } ] } ] }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` ### Suggestions The following events can be triggered for the Track Changes service. #### Suggestion added Name: `suggestion.added` Description: Triggered whenever a suggestion is added. ##### Payload * `document.id` – The ID of the document in which the suggestion was added. * `suggestion.id` – The ID of the suggestion. * `suggestion.created_at` – The creation date of the suggestion. * `suggestion.child_of` – The ID of the parent suggestion or null. * `suggestion.user.id` – The ID of the author of the suggestion. ##### Example The following example presents a webhook request sent after a suggestion is added. ``` { "event": "suggestion.added", "environment_id": "environment-1", "payload": { "document": { "id": "document-1" }, "suggestion": { "id": "suggestion-1", "created_at": "2019-05-29T08:17:53.450Z", "child_of": null, "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Suggestion accepted Name: `suggestion.accepted` Description: Triggered whenever a suggestion is accepted. ##### Payload * `document.id` – The ID of the document in which the suggestion was added. * `suggestion.id` – The ID of the suggestion. * `suggestion.created_at` – The creation date of the suggestion. * `suggestion.updated_at` – The date of the suggestion update/accept. * `suggestion.user.id` – The ID of the user who accepted the suggestion. ##### Example The following example presents a webhook request sent after a suggestion is accepted. ``` { "event": "suggestion.accepted", "environment_id": "environment-1", "payload": { "document": { "id": "document-1" }, "suggestion": { "id": "suggestion-1", "created_at": "2019-05-29T08:17:53.450Z", "updated_at": "2019-05-29T08:17:53.450Z", "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Suggestion rejected Name: `suggestion.rejected` Description: Triggered whenever a suggestion is rejected. ##### Payload * `document.id` – The ID of the document in which the suggestion was added. * `suggestion.id` – The ID of the suggestion. * `suggestion.created_at` – The creation date of the suggestion. * `suggestion.updated_at` – The date of the suggestion update/reject. * `suggestion.user.id` – The ID of the user who reject the suggestion. ##### Example The following example presents a webhook request sent after a suggestion is rejected. ``` { "event": "suggestion.rejected", "environment_id": "environment-1", "payload": { "document": { "id": "document-1" }, "suggestion": { "id": "suggestion-1", "created_at": "2019-05-29T08:17:53.450Z", "updated_at": "2019-05-29T08:17:53.450Z", "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Suggestion removed Name: `suggestion.removed` Description: Triggered whenever a suggestion is removed. ##### Payload * `document.id` – The ID of the document from which the suggestion was removed. * `suggestion.id` – The ID of the suggestion. * `suggestion.deleted_at` – The deletion date of the suggestion. * `suggestion.user.id` – The ID of the user who removed the suggestion. ##### Example The following example presents a webhook request sent after a suggestion is removed. ``` { "event": "suggestion.removed", "environment_id": "environment-1", "payload": { "document": { "id": "document-1" }, "suggestion": { "id": "suggestion-1", "deleted_at": "2019-05-29T08:17:53.450Z", "user": { "id": "user-id" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` #### Suggestion restored Name: `suggestion.restored` Description: Triggered whenever a suggestion is restored. A suggestion can be removed by removing the text in a document. The undo operation can restore the removed text but also restore the suggestion. ##### Payload * `document.id` – The ID of the document where the suggestion was restored. * `suggestion.id` – The ID of the suggestion. * `suggestion.restored_at` – The restoration date of the suggestion. * `suggestion.user.id` – The ID of the user who restored the suggestion. ##### Example The following example presents a webhook request sent after a suggestion is restored. ``` { "event": "suggestion.restored", "environment_id": "environment-1", "payload": { "document": { "id": "document-1" }, "suggestion": { "id": "suggestion-1", "restored_at": "2019-05-29T08:17:53.450Z", "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:56.761Z" } ``` ### Deprecated #### Collaboration session removed Replaced by: `collaboration.document.finished` and `collaboration.document.exported` Name: `collaboration.document.removed` (previously `document.removed`) Description: Triggered when a collaboration session for a document expires and the [temporary data of the document](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data) is removed. ##### Payload * `document.id` – The ID of the removed document. * `document.removed_at` – The date of the document removal. * `document.data` – The data of the removed document. This is an **optional parameter** that is set when the [document storage feature](#cs/latest/guides/collaboration/document-storage.html) is disabled and the [editorBundle](#cs/latest/guides/collaboration/editor-bundle.html) is uploaded for the environment. If some of the operations cannot be applied to the document, the `data` property is empty and the `collaboration.document.recovered` webhook is emitted. ##### Example The following example presents a webhook request sent after a document is removed. ``` { "event": "collaboration.document.removed", "environment_id": "environment-1", "payload": { "document": { "id": "document-1", "removed_at": "2019-05-29T08:17:56.761Z", "data": "

Document content

" } }, "sent_at": "2019-05-29T08:17:56.761Z" } ```
source file: "cs/latest/developer-resources/webhooks/management.html" ## Webhooks management Webhooks management is available for every environment in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for the On-Premises version. Webhooks are managed separately for every environment. The environment you will manage can be selected from the list of environments by clicking in a row with an environment of choice. ### Creating webhooks The webhooks can be managed on a dedicated page. To switch to the Webhooks section, click the `Webhooks` section in the left sidebar. The page shows existing webhooks and a button to add new ones. The list is empty until you define some webhooks for a particular environment. To create a new webhook, click the `Create webhook` button. The “Creating a webhook” page will show up. While creating a new webhook, you need to provide a `Webhook URL` that CKEditor Cloud Services will send the requests to. The provided endpoint must be available from the CKEditor Collaboration On-premises application. In the next section you can configure the retry mechanism, if enabled, the webhooks will be resent in the case when the response code from the Webhook URL endpoint will be larger than 299. The Webhook request will be retried for of number provided in the `Max retries` times, or until the response status 2xx will be returned. Between each retry, there will be a delay, depending on the `Retry Interval in minutes` value. The next section allows you to select [events](#cs/latest/developer-resources/webhooks/events.html) of which occurrence will trigger the webhook. A single webhook can be triggered by many events, depending on a need. [See the full list of available events in a dedicated article.](#cs/latest/developer-resources/webhooks/events.html) To finish the process you need to click the `Create webhook` button at the very bottom of the form. The newly created Webhook will appear on the list in the main view of the `Webhooks` tabs. ### Updating webhook Each webhook visible in this list can be edited. It can be done by clicking the `Edit` button. The button will open the “Editing a webhook” view. It presents exact same set of options as the “Create a new webhook” view. You can change the Webhook URL, the set of events that will trigger the webhook, and the settings of the retry mechanism. To save the changes you need to click the `Save changes` button at the very bottom of the form. You will be redirected to the list of created webhooks. ### Removing webhook To remove a webhook use the remove button with an icon of a trash bin next to the selected webhook. You will see the prompt to confirm the removal ### Next steps * [Read more about the request history in a dedicated article.](#cs/latest/developer-resources/webhooks/requests.html) * [Read more about testing webhooks in a dedicated article.](#cs/latest/developer-resources/webhooks/testing.html) * [Check the list of events available in CKEditor Cloud Services.](#cs/latest/developer-resources/webhooks/events.html) source file: "cs/latest/developer-resources/webhooks/overview.html" ## Webhooks Webhooks resemble a notification mechanism that can be used to build integrations with CKEditor Cloud Services. CKEditor Cloud Services sends an HTTP `POST` request to a configured URL when [specified events](#cs/latest/developer-resources/webhooks/events.html) are triggered. Webhooks can be used for data synchronization between CKEditor Cloud Services and another system or to build a notifications system. For example, thanks to webhooks, the system might notify the users via email about changes made in the document. ### Benefits of using webhooks By using webhooks while integrating your application with the CKEditor Collaboration Server you can make the integration more reliable. Your application will send requests to the CKEditor Collaboration Server only when it is needed, without any periodical requests. It will limit the transfer between both servers and the complexity of implementation, as webhooks are based on REST API endpoints. Webhooks are called almost immediately after specific event occur. #### Examples ##### Synchronization of comments One way of using webhooks can be synchronizing comments with your server. The application can register for [comments-related events](#cs/latest/developer-resources/webhooks/events.html--comments) and add, remove and update comments in a external database according to data included in webhook event’s payloads. ##### Comments notification Another case where webhooks can be useful is a situation where editors needs to be informed about new comments in a document. Application can register for the `comments.added` webhook event, and after handling it can send email by using any external service providers. ##### Saving document The example of saving document with usage of webhooks and REST API is described in a [Initializing and saving documents](#cs/latest/guides/collaboration/introduction.html--saving-data-with-webhooks) guide. ##### User access monitoring There may be a need for receiving information who and when connected to the document. It may be easy done with `user.connected` and `user.disconnected` event. Information who, when and for how long accessed the document may be collected by your application. ### Security Every webhook request sent from CKEditor Cloud Services to the configured webhook URL has a signature and a timestamp. Thanks to this, every request received by the server can be checked and confirmed that it comes from CKEditor Cloud Services and has not been modified. More information about the verification process of signing requests can be found in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. ### Webhook order Please keep in mind that webhook events are sent asynchronously. You should, therefore, not rely on the order of the received events. If operations you are performing are order-sensitive, you should add another layer of verification to your endpoint that handles events. ### Webhook format Each webhook request sent by CKEditor Cloud Services has the following properties: * `event` – The name of the event that triggered the webhook. * `environment_id` – The ID of the environment. * `sent_at` – The date when the specified webhook was sent. * `payload` – The payload of the webhook. It contains the data about a specific event. #### Example Below you can find an example of a sample webhook request sent by CKEditor Cloud Services. It is triggered by a comment added to the comment thread `thread-1` in the document with an ID of `doc-1` by the user with an ID of `user-1`. ``` { "event": "comment.added", "environment_id": "environment-1", "payload": { "document": { "id": "doc-1" }, "comment": { "id": "comment-1", "created_at": "2019-05-29T08:17:53.450Z", "content": "Some comment content.", "thread_id": "thread-1", "user": { "id": "user-1" } } }, "sent_at": "2019-05-29T08:17:53.457Z" } ``` #### Example In case of a mechanism, where documents are received through the `collaboration.document.exported` webhook and stored in an external database, the `document.removed_at` field from the payload of the webhook should be compared with a date from a previous event. It will secure application in a case where the order of a received event is changed and the current document will be overridden with an older version from an event which was received after the latest version. If the `document.removed_at` is older than the date already stored in the database, then saving documents should be skipped. It helps you to avoid inconsistency between states of documents in databases. ### Next steps * Check the [list of events available in CKEditor Cloud Services](#cs/latest/developer-resources/webhooks/events.html). * Learn how to [manage existing webhooks and create new ones](#cs/latest/developer-resources/webhooks/management.html). source file: "cs/latest/developer-resources/webhooks/requests.html" ## Requests history In the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises, for each environment, there is a list of sent requests for each created webhook. Each request on the list can be resent and contains some relevant and useful information, such as: * `request payload`, * `request headers`, * `response status`, * `response body`, * `response headers`. The view of a particular request allows you to resent the webhook with exactly the same data, as during the first call. ### Display requests To display a list of requests sent by a particular webhook, you need to display a list of created webhooks for a particular environment. Then click the selected webhook from the list. You will see the paginated list of the webhook’s requests. It comes with basic information like response status, the name of the event which triggered the webhook, and the time when it was sent. ### Filtering requests The list of requests usually contains hundreds of requests. To make reviewing them easier, there is a possibility to filter them. By using the filters above the list you can filter the request by response status, events, and range of time when they ocurred. By default, the requests from the last hour are displayed. ### Details of request To check the details of a particular request click the icon with an arrow on the left side of the selected row. After selecting a particular request, you will see its details. You can switch between Request and Response details, by using the `Request` and `Response` tabs at the top. ### Manual resent In case of failure of a server that receives the webhooks requests, you may need to manually resent a specific request to not lose data. The request details view allows you to resend the request by using the `Resend request` button. You will be asked to confirm the action. After the request will be resent successfully, you see the list of requests with the new request added. ### Next steps * [Read more about testing webhooks in a dedicated article.](#cs/latest/developer-resources/webhooks/testing.html) * [Check the list of events available in CKEditor Cloud Services.](#cs/latest/developer-resources/webhooks/events.html) source file: "cs/latest/developer-resources/webhooks/server.html" ## Webhooks server To work with incoming webhooks, an application that listens on HTTP `POST` requests sent from CKEditor Cloud Services is needed. **It is necessary that this application is accessible from the Internet.** ### Example Check an [example of a webhooks server implementation in Node.js](#cs/latest/examples/webhooks/webhooks-server-nodejs.html). Check an [example of a webhooks server implementation in PHP](#cs/latest/examples/webhooks/webhooks-server-php.html). source file: "cs/latest/developer-resources/webhooks/testing.html" ## Webhooks testing A crucial part of any integration is testing. To this end, CKEditor Cloud Services provides several tools that facilitate the testing of integration with webhooks. ### Ping event After creating a webhook, CKEditor Cloud Services sends a straightforward `ping` event that can be used to confirm that the integration works correctly. Payload: * `id` – The ID of the request. * `url` – The URL of the request. #### Example You can find an example of a `ping` event sent by CKEditor Cloud Services below. ``` { "event": "ping", "environment_id": "environment-1", "payload": { "id": "id", "url": "url", }, "sent_at": "2020-06-15T09:32:17.813Z" } ``` ### Next steps * [Read more about managing webhooks in a dedicated article.](#cs/latest/developer-resources/webhooks/management.html) * [Read more about the request history in a dedicated article.](#cs/latest/developer-resources/webhooks/requests.html) * [Check the list of events available in CKEditor Cloud Services.](#cs/latest/developer-resources/webhooks/events.html) source file: "cs/latest/examples/collaboration-examples/import-export-nodejs.html" ## Import and export feature in Node.js This article presents an example of an application that uses the [import and export feature](#cs/latest/guides/collaboration/import-and-export.html) in Node.js and Express.js. ### Dependencies This example uses the following dependencies: * `axios` * `body-parser` * `cors` * `express` It also uses core dependencies from Node.js: `path` and `fs`. ### Example The following example allows you to upload an [editor bundle](#cs/latest/examples/editor-bundle/editor-bundle-nodejs.html), import the document data to CKEditor Cloud Services, and export the collaboration content. This file presents an example of a simple `Express.js` application. ``` // index.js const path = require( 'path' ); const fs = require( 'fs' ); const express = require( 'express' ); const axios = require( 'axios' ); const cors = require( 'cors' ); const bodyParser = require( 'body-parser' ); const generateSignature = require( './utils/generateSignature' ); // See: https://ckeditor.com/docs/cs/latest/examples/security/request-signature-nodejs.html. const editorBundle = fs.readFileSync( path.resolve( '../client/build/ckeditor.js' ) ); // This should be your bundled editor. const app = express(); const port = 8000; // The default application port. const bundleVersion = 'bundleVersion'; // This value should be unique per environment. const apiSecret = 'SECRET'; // Do not forget to hide this value in a safe place e.g. an .env file! const organizationId = 'organizationId'; // Type your organization ID here. const environmentId = 'environmentId'; // Type your environment ID here. // If you use On-Premises application you can adjust baseApiUrl accordingly with your application URL. const baseApiUrl = `https://${ organizationId }.cke-cs.com/api/v5/${ environmentId }`; app.use( bodyParser.urlencoded( { extended: true } ) ); app.use( bodyParser.json() ); app.use( cors() ); // This function will be responsible for sending requests to CKEditor Cloud Services API. async function sendRequest( method, url, body ) { const CSTimestamp = Date.now(); const payload = { method, url, mode: 'no-cors', headers: { 'Content-Type': 'application/json', 'X-CS-Signature': generateSignature( apiSecret, method, url, CSTimestamp, body ), 'X-CS-Timestamp': CSTimestamp } }; if ( method.toUpperCase() !== 'GET' ) { payload.data = body; } try { const { status, data } = await axios( payload ); return { status, data }; } catch ( { response } ) { const { status, data } = response; return { status, data }; } } // Upload the editor bundle. Note that you will need to upload your editor again if you change the bundle. app.post( '/upload-editor', async ( req, res ) => { const { bundleVersion } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/editors`, { bundle: editorBundle.toString(), config: { cloudServices: { bundleVersion } } } ); return res.json( { status, data } ); } ); // Import content to the document. app.post( '/import', async ( req, res ) => { const { documentId, documentContent, bundleVersion } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/collaborations`, { document_id: documentId, bundle_version: bundleVersion, data: documentContent } ); return res.json( { status, data } ); } ); // Export your data from CKEditor Cloud Services. You can schedule export operations e.g. once per hour. app.get( '/export/:documentId', async ( req, res ) => { const { documentId } = req.params; const { status, data } = await sendRequest( 'GET', `${ baseApiUrl }/collaborations/${ documentId }` ); return res.json( { status, data } ); } ); app.listen( port, () => console.log( `The application is listening on port ${ port }!` ) ); ``` ### Usage Run: ``` node index.js ``` You can then perform actions and communicate with CKEditor Cloud Services by sending HTTP requests to this application. See the following example: 1. Uploading editor bundle. ``` try { const response = await fetch( 'http://localhost:8000/upload-editor', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { bundleVersion } ) } ); const data = await response.json(); console.log( 'Result of uploading editor:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Importing the collaboration session. ``` try { const response = await fetch( 'http://localhost:8000/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { bundleVersion, documentId: 'document-1', documentContent: '

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

', } ) } ); const data = await response.json(); console.log( 'Result of importing collaborative editing session:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Exporting the collaboration session. The content imported in step 2 should be returned. ``` try { const response = await fetch( `http://localhost:8000/export/document-1`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ); const data = await response.json(); console.log( 'Result of exporting collaborative editing session:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ```
source file: "cs/latest/examples/collaboration-examples/migrating-document-to-rtc.html" ## Migrating to RTC in Node.js This article presents an example of an application that allows for migrating users and documents created in non Real-time Collaboration to Real-time Collaboration. ### Dependencies This example uses the following dependencies: * `axios` * `body-parser` * `cors` * `express` It also uses core dependencies from Node.js: `path` and `fs`. ### Example The following example allows you to upload an [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html), create a [user](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html) and [import a document to the Real-time Collaboration](#cs/latest/guides/basic-concepts/data-model.html--document). The following example will cover: * Uploading an editor bundle to CKEditor Cloud Services * Creating a Real-time Collaboration user * Mapping the document and all its data from the non Real-time Collaboration and importing to the CKEditor Cloud Services Presented below is an example of a simple `Express.js` application. ``` // mapDocumentData.js /** * Maps all data for the document to the structure and assigns data missing in non-RTC * according to the CKEditor Cloud Services' requirements. * * @param {Object} [documentData] The whole data of the document with all its resources saved from the non Real-time Collaboration. * @param {String} [documentData.documentId] The ID of the document to import * @param {String} [documentData.bundleVersion] The version of the bundled editor to use for the document * @param {String} [documentData.editorData] The content of the document in HTML or Markdown * @param {Array.} [documentData.commentThreadsData] * @param {Array.} [documentData.suggestionsData] * @param {Array.} [documentData.revisionsData] * @param {String} [suggestionAuthorID] the ID of the user to assign for suggestions that don't have an author assigned to them * @returns {Object} */ function mapDocumentData( documentData = {}, suggestionAuthorID ) { const threads = []; const comments = []; const { commentThreadsData = [], suggestionsData = [], revisionsData = [], documentId, bundleVersion, editorData } = documentData; commentThreadsData.forEach( commentThread => { const hasMatchingSuggestion = suggestionsData.some( suggestion => suggestion.id === commentThread.id ); threads.push( { id: commentThread.threadId, context: commentThread.context, resolved_at: commentThread.resolvedAt, resolved_by: commentThread.resolvedBy, attributes: commentThread.attributes, author_id: commentThread.authorId } ); comments.push( ...commentThread.comments.map( comment => ( { id: comment.commentId, thread_id: commentThread.threadId, content: comment.content, attributes: comment.attributes, user: { id: comment.authorId }, created_at: comment.created_at, type: hasMatchingSuggestion ? 2 : 1 } ) ) ); } ); const revisions = []; let version = 1; revisionsData.forEach( ( revision, index ) => { if ( revision.toVersion > version ) { version = revision.toVersion; } revisions.push( { revision_id: revision.id, request_id: index, name: revision.name, creator_id: revision.authorId, authors_ids: revision.authors_ids, diff_data: JSON.stringify( revision.diffData ), created_at: revision.createdAt, attributes: revision.attributes, from_version: revision.fromVersion, to_version: revision.toVersion } ) } ); const suggestions = suggestionsData.map( suggestion => ( { id: suggestion.id, type: suggestion.type, author_id: suggestion.authorId ?? suggestionAuthorID, created_at: suggestion.createdAt, has_comments: suggestion.hasComments, data: suggestion.data, attributes: suggestion.attributes, } ) ) const importBody = { id: documentId, content: { bundle_version: bundleVersion, data: editorData, version: version ?? undefined }, comments, suggestions, revisions, threads }; return importBody; } ``` ``` // index.js const path = require( 'path' ); const fs = require( 'fs' ); const express = require( 'express' ); const axios = require( 'axios' ); const cors = require( 'cors' ); const bodyParser = require( 'body-parser' ); const generateSignature = require( './utils/generateSignature' ); // See: https://ckeditor.com/docs/cs/latest/examples/security/request-signature-nodejs.html. const mapDocumentData = require( './utils/mapDocumentData' ); const editorBundle = fs.readFileSync( path.resolve( '../client/build/ckeditor.js' ) ); // It should be your bundled editor. const app = express(); const port = 8000; // The default application port. const apiSecret = 'SECRET'; // Do not forget to hide this value in a safe place e.g. a .env file! const organizationId = 'organizationId'; // Type your organization ID here. const environmentId = 'environmentId'; // Type your environment ID here. // If you use On-Premises application you can adjust baseApiUrl accordingly with your application URL. const baseApiUrl = `https://${ organizationId }.cke-cs.com/api/v5/${ environmentId }`; app.use( bodyParser.urlencoded( { extended: true } ) ); app.use( bodyParser.json() ); app.use( cors() ); // This function will be responsible for sending requests to CKEditor Cloud Services API. async function sendRequest( method, url, body ) { const CSTimestamp = Date.now(); const payload = { method, url, mode: 'no-cors', headers: { 'Content-Type': 'application/json', 'X-CS-Signature': generateSignature( apiSecret, method, url, CSTimestamp, body ), 'X-CS-Timestamp': CSTimestamp } }; if ( method.toUpperCase() !== 'GET' ) { payload.data = body; } try { const { status, data } = await axios( payload ); return { status, data }; } catch ( { response } ) { const { status, data } = response; return { status, data }; } } // Upload the editor bundle. Note that you will need to upload your editor again if you change the bundle. app.post( '/upload-editor', async ( req, res ) => { const { bundleVersion } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/editors`, { bundle: editorBundle.toString(), config: { cloudServices: { bundleVersion // This value should be unique per environment. } } } ); return res.json( { status, data } ); } ); // Create a user for Real-time Collaboration. app.post( '/create-user', async ( req, res ) => { const { email, id, name } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/users`, { id, email, name } ); return res.json( { status, data } ); } ); // Import a document to the CKEditor Cloud Services. app.post( '/import-document', async ( req, res ) => { const { documentData, suggestionAuthorId } = req.body; const importBody = mapDocumentData( documentData, suggestionAuthorId ); const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/documents`, importBody ); return res.json( { status, data } ); } ); app.listen( port, () => console.log( `The application is listening on port ${ port }!` ) ); ``` ### Usage Run: ``` node index.js ``` By sending HTTP requests to this application you can now perform actions and communicate with CKEditor Cloud Services. See the following example: 1. Uploading editor bundle. ``` try { const response = await fetch( 'http://localhost:8000/upload-editor', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { bundleVersion: '1.0.0' } ) } ); const data = await response.json(); console.log( 'Result of uploading editor:', data ); } catch ( error ) { console.log( 'Error occurred when uploading editor:', error ); } ``` 1. Creating a user. ``` try { const response = await fetch( 'http://localhost:8000/create-user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { id: 'user-1', email: 'example@example.com', name: 'Example user' } ) } ); const data = await response.json(); console.log( 'Result of uploading user:', data ); } catch ( error ) { console.log( 'Error occurred when uploading user:', error ); } ``` 1. Importing a document to the CKEditor Cloud Services. For `documentData` you can either use the data taken from your server or you can use the example below. Click to see the example data. ``` { "documentId": "document-1", "bundleVersion": "1.0.0", "editorData": "

NON-DISCLOSURE AGREEMENT (NDA)

 

This Nondisclosure Agreement or (\"Agreement\") has been entered into on the the date of 10.05.2021 and is by and between:

Party Disclosing Information: James Lahey with a mailing address of 3 Maple Rd. Spryfield B3P 1H8  (\"Disclosing Party\").

Party Receiving Information:  Richard Lafleur with a mailing address of  1 Bonnyville Dr. Spryfield B3P 1H8 (\"Receiving Party\"). For the purpose of preventing the unauthorized disclosure of Confidential Information as defined below. The parties agree to enter into a confidential relationship concerning the disclosure of certain proprietarybusiness property and confidential information (\"Confidential Information\").

 

1. Definition of Confidential Information

For purposes of this Agreement, \"Confidential Information\" shall include all information or material that has or could have commercial value or other utility in the business in which the Disclosing Party is engaged. If Confidential Information is in written form, the Disclosing Party shall label or stamp the materials with the word \"Confidential\" or some similar warning. If Confidential Information is transmitted orally, the Disclosing Party shall promptly provide a writing indicating that such oral communication constituted Confidential Information.

2. Exclusions from Confidential Information

Receiving Party's obligations under this Agreement do not extend to information that is:(a) publicly known at the time of disclosure or subsequently becomes publicly known through no fault of the Receiving Party; (b) discovered or created by the Receiving Party before disclosure by Disclosing Party; (c) learned by the Receiving Party through legitimate means other than from the Disclosing Party or Disclosing Party's representatives; or (d) is disclosed by Receiving Party with Disclosing Party's prior written approval.

3. Obligations of Receiving Party

Receiving Party shall hold and maintain the Confidential Information in strictest confidence for the sole and exclusive benefit of the Disclosing Party. Receiving Party shall carefully restrict access to Confidential Information to employees, contractors and third parties as is reasonably required and shall require those persons to sign nondisclosure restrictions at least as protective as those in this Agreement. Receiving Party shall not, without the prior written approval of Disclosing Party, use for Receiving Party's benefit, publish, copy, or otherwise disclose to others, or permit the use by others for their benefit or to the detriment of Disclosing Party, any Confidential Information. Receiving Party shall return to Disclosing Party any and all records, notes, and other written, printed, or tangible materials in its possession pertaining to Confidential Information immediately if Disclosing Party requests it in writing.

4. Time Periods

The nondisclosure provisions of this Agreement shall survive the termination of this Agreement and Receiving Party's duty to hold Confidential Information in confidence shall remain in effect until the Confidential Information no longer qualifies as a trade secret or until Disclosing Party sends Receiving Party written notice releasing Receiving Party from this Agreement, whichever occurs first.

5. Relationships

Nothing contained in this Agreement shall be deemed to constitute either party a partner, joint venture or employee of the other party for any purpose.

6. Severability

If a court finds any provision of this Agreement invalid or unenforceable, the remainder of this Agreement shall be interpreted so as best to affect the intent of the parties.

7. Integration

This Agreement expresses the complete understanding of the parties with respect to the subject matter and supersedes all prior proposals, agreements, representations, and understandings. This Agreement may not be amended except in writing signed by both parties.

8. Waiver

The failure to exercise any right provided in this Agreement shall not be a waiver of prior or subsequent rights.

9. Notice of Immunity

Employee is provided notice that an individual shall not be held criminally or civilly liable under any federal or state trade secret law for the disclosure of a trade secret that is made (i) in confidence to a federal, state, or local government official, either directly or indirectly, or to an attorney; and (ii) solely for the purpose of reporting or investigating a suspected violation of law; or is made in a complaint or other document filed in a lawsuit or other proceeding, if such filing is made under seal. An individual who files a lawsuit for retaliation by an employer for reporting a suspected violation of law may disclose the trade secret to the attorney of the individual and use the trade secret information in the court proceeding, if the individual (i) files any document containing the trade secret under seal; and (ii) does not disclose the trade secret, except pursuant to court order. This Agreement and each party's obligations shall be binding on the representatives, assigns and successors of such party. Each party has signed this Agreement through its authorized representative.

 

 

DISCLOSING PARTY Signature: _____________________________________________________

Typed or Printed Name: James Lahey

Date: 10.05.2021

 

RECEIVING PARTY Signature: _____________________________________________________

Typed or Printed Name: Richard Lafleur

Date: 10.05.2021

", "commentThreadsData": [ { "threadId": "eca0c3e71883a578ee37bdc0e510a41a8", "context": { "type": "text", "value": "3 Maple Rd. Spryfield B3P 1H8" }, "authorId": "user-1", "resolvedAt": null, "resolvedBy": null, "comments": [ { "commentId": "e949f2f153f07ddc9b5f161062d4a1ad9", "content": "

This might be Mr. Lafleur's address. Can we double-check the addresses?

", "createdAt": "2023-08-24T09:35:17.338Z", "authorId": "user-1", "attributes": {} } ], "attributes": {} }, { "threadId": "e5964fb81e95e0cd712ecf95fafad425f", "context": { "type": "text", "value": "(i)" }, "authorId": "user-1", "resolvedAt": null, "resolvedBy": null, "comments": [ { "commentId": "edadb356033be84bb93085092e4d8b15b", "content": "

Maybe we should change this listing to be consistent with the one from section 2

", "createdAt": "2023-08-24T09:37:11.805Z", "authorId": "user-1", "attributes": {} } ], "attributes": {} } ], "suggestionsData": [ { "id": "ea6c5028e3c0efaf0b9a23d227d89696c", "type": "deletion", "createdAt": "2023-08-24T09:31:40.515Z", "hasComments": false, "data": null, "attributes": {} }, { "id": "e513f8e2e4eca79825823ff00294f3716", "type": "deletion", "createdAt": "2023-08-24T09:32:02.186Z", "hasComments": false, "data": null, "attributes": {} }, { "id": "e64910f0cd32232aac1cce1c6b5b565b6", "type": "insertion", "createdAt": "2023-08-24T09:32:02.186Z", "hasComments": false, "data": null, "attributes": {} } ], "revisionsData": [ { "id": "initial", "name": "Empty document", "creatorId": "user-1", "authorsIds": [], "diffData": { "main": { "insertions": "[{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]}]", "deletions": "[{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]}]", "attachChange": null, "attributesBefore": {}, "attributesAfter": {} } }, "createdAt": "2023-08-24T09:28:43.499Z", "attributes": {}, "fromVersion": 2, "toVersion": 2 }, { "id": "ed433b9857b18e2ece0ca0b19a0288ba5", "name": "revision-1", "creatorId": "user-1", "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": "[{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[[\"data-revision-start-before\",\"insertion:user-1:0\"],[\"style\",\"text-align:center;\"]],\"children\":[\"NON-DISCLOSURE AGREEMENT (NDA)\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Nondisclosure Agreement or (\\\"Agreement\\\") has been entered into on the \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"the \",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"date of 10.05.2021 and is by and between:\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Disclosing Information:\"]},\" James Lahey with a mailing address of 3 Maple Rd. Spryfield B3P 1H8 (\\\"Disclosing Party\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Receiving Information:\"]},\" Richard Lafleur with a mailing address of 1 Bonnyville Dr. Spryfield B3P 1H8 (\\\"Receiving Party\\\"). For the purpose of preventing the unauthorized disclosure of Confidential Information as defined below. The parties agree to enter into a confidential relationship concerning the disclosure of certain \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},\"proprietary\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\"business\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\" property and confidential information (\\\"Confidential Information\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"1. Definition of Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"For purposes of this Agreement, \\\"Confidential Information\\\" shall include all information or material that has or could have commercial value or other utility in the business in which the Disclosing Party is engaged. If Confidential Information is in written form, the Disclosing Party shall label or stamp the materials with the word \\\"Confidential\\\" or some similar warning. If Confidential Information is transmitted orally, the Disclosing Party shall promptly provide a writing indicating that such oral communication constituted Confidential Information.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"2. Exclusions from Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party's obligations under this Agreement do not extend to information that is:(a) publicly known at the time of disclosure or subsequently becomes publicly known through no fault of the Receiving Party; (b) discovered or created by the Receiving Party before disclosure by Disclosing Party; (c) learned by the Receiving Party through legitimate means other than from the Disclosing Party or Disclosing Party's representatives; or (d) is disclosed by Receiving Party with Disclosing Party's prior written approval.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"3. Obligations of Receiving Party\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party shall hold and maintain the Confidential Information in strictest confidence for the sole and exclusive benefit of the Disclosing Party. Receiving Party shall carefully restrict access to Confidential Information to employees, contractors and third parties as is reasonably required and shall require those persons to sign nondisclosure restrictions at least as protective as those in this Agreement. Receiving Party shall not, without the prior written approval of Disclosing Party, use for Receiving Party's benefit, publish, copy, or otherwise disclose to others, or permit the use by others for their benefit or to the detriment of Disclosing Party, any Confidential Information. Receiving Party shall return to Disclosing Party any and all records, notes, and other written, printed, or tangible materials in its possession pertaining to Confidential Information immediately if Disclosing Party requests it in writing.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"4. Time Periods\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The nondisclosure provisions of this Agreement shall survive the termination of this Agreement and Receiving Party's duty to hold Confidential Information in confidence shall remain in effect until the Confidential Information no longer qualifies as a trade secret or until Disclosing Party sends Receiving Party written notice releasing Receiving Party from this Agreement, whichever occurs first.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"5. Relationships\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Nothing contained in this Agreement shall be deemed to constitute either party a partner, joint venture or employee of the other party for any purpose.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"6. Severability\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"If a court finds any provision of this Agreement invalid or unenforceable, the remainder of this Agreement shall be interpreted so as best to affect the intent of the parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"7. Integration\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Agreement expresses the complete understanding of the parties with respect to the subject matter and supersedes all prior proposals, agreements, representations, and understandings. This Agreement may not be amended except in writing signed by both parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"8. Waiver\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The failure to exercise any right provided in this Agreement shall not be a waiver of prior or subsequent rights.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"9. Notice of Immunity\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Employee is provided notice that an individual shall not be held criminally or civilly liable under any federal or state trade secret law for the disclosure of a trade secret that is made (i) in confidence to a federal, state, or local government official, either directly or indirectly, or to an attorney; and (ii) solely for the purpose of reporting or investigating a suspected violation of law; or is made in a complaint or other document filed in a lawsuit or other proceeding, if such filing is made under seal. An individual who files a lawsuit for retaliation by an employer for reporting a suspected violation of law may disclose the trade secret to the attorney of the individual and use the trade secret information in the court proceeding, if the individual (i) files any document containing the trade secret under seal; and (ii) does not disclose the trade secret, except pursuant to court order. This Agreement and each party's obligations shall be binding on the representatives, assigns and successors of such party. Each party has signed this Agreement through its authorized representative.\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"DISCLOSING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" James Lahey\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"RECEIVING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" Richard Lafleur\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[[\"data-revision-end-after\",\"insertion:user-1:0\"]],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]}]", "deletions": "[{\"type\":\"c\",\"name\":\"p\",\"attributes\":[[\"data-revision-end-after\",\"deletion:user-1:0\"],[\"data-revision-start-before\",\"deletion:user-1:0\"]],\"children\":[]}]", "attachChange": null, "attributesBefore": {}, "attributesAfter": {} } }, "createdAt": "2023-08-24T09:33:51.568Z", "attributes": {}, "fromVersion": 2, "toVersion": 14 }, { "id": "e6a32e19aceae7a67bb9861a0b887611b", "name": "", "creatorId": null, "authorsIds": [ "user-1" ], "diffData": { "main": { "insertions": "[{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[[\"style\",\"text-align:center;\"]],\"children\":[\"NON-DISCLOSURE AGREEMENT (NDA)\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Nondisclosure Agreement or (\\\"Agreement\\\") has been entered into on the \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"the \",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"date of 10.05.2021 and is by and between:\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Disclosing Information:\"]},\" James Lahey with a mailing address of \",{\"type\":\"u\",\"name\":\"comment-start\",\"attributes\":[[\"name\",\"eca0c3e71883a578ee37bdc0e510a41a8:49cf1\"]],\"children\":[]},\"3 Maple Rd. Spryfield B3P 1H8\",{\"type\":\"u\",\"name\":\"comment-end\",\"attributes\":[[\"name\",\"eca0c3e71883a578ee37bdc0e510a41a8:49cf1\"]],\"children\":[]},\" (\\\"Disclosing Party\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Receiving Information:\"]},\" Richard Lafleur with a mailing address of 1 Bonnyville Dr. Spryfield B3P 1H8 (\\\"Receiving Party\\\"). For the purpose of preventing the unauthorized disclosure of Confidential Information as defined below. The parties agree to enter into a confidential relationship concerning the disclosure of certain \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},\"proprietary\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\"business\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\" property and confidential information (\\\"Confidential Information\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"1. Definition of Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"For purposes of this Agreement, \\\"Confidential Information\\\" shall include all information or material that has or could have commercial value or other utility in the business in which the Disclosing Party is engaged. If Confidential Information is in written form, the Disclosing Party shall label or stamp the materials with the word \\\"Confidential\\\" or some similar warning. If Confidential Information is transmitted orally, the Disclosing Party shall promptly provide a writing indicating that such oral communication constituted Confidential Information.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"2. Exclusions from Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party's obligations under this Agreement do not extend to information that is:(a) publicly known at the time of disclosure or subsequently becomes publicly known through no fault of the Receiving Party; (b) discovered or created by the Receiving Party before disclosure by Disclosing Party; (c) learned by the Receiving Party through legitimate means other than from the Disclosing Party or Disclosing Party's representatives; or (d) is disclosed by Receiving Party with Disclosing Party's prior written approval.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"3. Obligations of Receiving Party\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party shall hold and maintain the Confidential Information in strictest confidence for the sole and exclusive benefit of the Disclosing Party. Receiving Party shall carefully restrict access to Confidential Information to employees, contractors and third parties as is reasonably required and shall require those persons to sign nondisclosure restrictions at least as protective as those in this Agreement. Receiving Party shall not, without the prior written approval of Disclosing Party, use for Receiving Party's benefit, publish, copy, or otherwise disclose to others, or permit the use by others for their benefit or to the detriment of Disclosing Party, any Confidential Information. Receiving Party shall return to Disclosing Party any and all records, notes, and other written, printed, or tangible materials in its possession pertaining to Confidential Information immediately if Disclosing Party requests it in writing.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"4. Time Periods\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The nondisclosure provisions of this Agreement shall survive the termination of this Agreement and Receiving Party's duty to hold Confidential Information in confidence shall remain in effect until the Confidential Information no longer qualifies as a trade secret or until Disclosing Party sends Receiving Party written notice releasing Receiving Party from this Agreement, whichever occurs first.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"5. Relationships\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Nothing contained in this Agreement shall be deemed to constitute either party a partner, joint venture or employee of the other party for any purpose.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"6. Severability\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"If a court finds any provision of this Agreement invalid or unenforceable, the remainder of this Agreement shall be interpreted so as best to affect the intent of the parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"7. Integration\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Agreement expresses the complete understanding of the parties with respect to the subject matter and supersedes all prior proposals, agreements, representations, and understandings. This Agreement may not be amended except in writing signed by both parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"8. Waiver\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The failure to exercise any right provided in this Agreement shall not be a waiver of prior or subsequent rights.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"9. Notice of Immunity\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Employee is provided notice that an individual shall not be held criminally or civilly liable under any federal or state trade secret law for the disclosure of a trade secret that is made \",{\"type\":\"u\",\"name\":\"comment-start\",\"attributes\":[[\"name\",\"e5964fb81e95e0cd712ecf95fafad425f:57cf0\"]],\"children\":[]},\"(i)\",{\"type\":\"u\",\"name\":\"comment-end\",\"attributes\":[[\"name\",\"e5964fb81e95e0cd712ecf95fafad425f:57cf0\"]],\"children\":[]},\" in confidence to a federal, state, or local government official, either directly or indirectly, or to an attorney; and (ii) solely for the purpose of reporting or investigating a suspected violation of law; or is made in a complaint or other document filed in a lawsuit or other proceeding, if such filing is made under seal. An individual who files a lawsuit for retaliation by an employer for reporting a suspected violation of law may disclose the trade secret to the attorney of the individual and use the trade secret information in the court proceeding, if the individual (i) files any document containing the trade secret under seal; and (ii) does not disclose the trade secret, except pursuant to court order. This Agreement and each party's obligations shall be binding on the representatives, assigns and successors of such party. Each party has signed this Agreement through its authorized representative.\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"DISCLOSING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" James Lahey\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"RECEIVING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" Richard Lafleur\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]}]", "deletions": "[{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[[\"style\",\"text-align:center;\"]],\"children\":[\"NON-DISCLOSURE AGREEMENT (NDA)\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Nondisclosure Agreement or (\\\"Agreement\\\") has been entered into on the \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"the \",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:ea6c5028e3c0efaf0b9a23d227d89696c:user-1\"]],\"children\":[]},\"date of 10.05.2021 and is by and between:\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Disclosing Information:\"]},\" James Lahey with a mailing address of 3 Maple Rd. Spryfield B3P 1H8 (\\\"Disclosing Party\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Party Receiving Information:\"]},\" Richard Lafleur with a mailing address of 1 Bonnyville Dr. Spryfield B3P 1H8 (\\\"Receiving Party\\\"). For the purpose of preventing the unauthorized disclosure of Confidential Information as defined below. The parties agree to enter into a confidential relationship concerning the disclosure of certain \",{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},\"proprietary\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"insertion:e64910f0cd32232aac1cce1c6b5b565b6:user-1\"]],\"children\":[]},{\"type\":\"u\",\"name\":\"suggestion-start\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\"business\",{\"type\":\"u\",\"name\":\"suggestion-end\",\"attributes\":[[\"name\",\"deletion:e513f8e2e4eca79825823ff00294f3716:user-1\"]],\"children\":[]},\" property and confidential information (\\\"Confidential Information\\\").\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"1. Definition of Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"For purposes of this Agreement, \\\"Confidential Information\\\" shall include all information or material that has or could have commercial value or other utility in the business in which the Disclosing Party is engaged. If Confidential Information is in written form, the Disclosing Party shall label or stamp the materials with the word \\\"Confidential\\\" or some similar warning. If Confidential Information is transmitted orally, the Disclosing Party shall promptly provide a writing indicating that such oral communication constituted Confidential Information.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"2. Exclusions from Confidential Information\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party's obligations under this Agreement do not extend to information that is:(a) publicly known at the time of disclosure or subsequently becomes publicly known through no fault of the Receiving Party; (b) discovered or created by the Receiving Party before disclosure by Disclosing Party; (c) learned by the Receiving Party through legitimate means other than from the Disclosing Party or Disclosing Party's representatives; or (d) is disclosed by Receiving Party with Disclosing Party's prior written approval.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"3. Obligations of Receiving Party\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Receiving Party shall hold and maintain the Confidential Information in strictest confidence for the sole and exclusive benefit of the Disclosing Party. Receiving Party shall carefully restrict access to Confidential Information to employees, contractors and third parties as is reasonably required and shall require those persons to sign nondisclosure restrictions at least as protective as those in this Agreement. Receiving Party shall not, without the prior written approval of Disclosing Party, use for Receiving Party's benefit, publish, copy, or otherwise disclose to others, or permit the use by others for their benefit or to the detriment of Disclosing Party, any Confidential Information. Receiving Party shall return to Disclosing Party any and all records, notes, and other written, printed, or tangible materials in its possession pertaining to Confidential Information immediately if Disclosing Party requests it in writing.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"4. Time Periods\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The nondisclosure provisions of this Agreement shall survive the termination of this Agreement and Receiving Party's duty to hold Confidential Information in confidence shall remain in effect until the Confidential Information no longer qualifies as a trade secret or until Disclosing Party sends Receiving Party written notice releasing Receiving Party from this Agreement, whichever occurs first.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"5. Relationships\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Nothing contained in this Agreement shall be deemed to constitute either party a partner, joint venture or employee of the other party for any purpose.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"6. Severability\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"If a court finds any provision of this Agreement invalid or unenforceable, the remainder of this Agreement shall be interpreted so as best to affect the intent of the parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"7. Integration\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"This Agreement expresses the complete understanding of the parties with respect to the subject matter and supersedes all prior proposals, agreements, representations, and understandings. This Agreement may not be amended except in writing signed by both parties.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"8. Waiver\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"The failure to exercise any right provided in this Agreement shall not be a waiver of prior or subsequent rights.\"]},{\"type\":\"c\",\"name\":\"h2\",\"attributes\":[],\"children\":[\"9. Notice of Immunity\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[\"Employee is provided notice that an individual shall not be held criminally or civilly liable under any federal or state trade secret law for the disclosure of a trade secret that is made (i) in confidence to a federal, state, or local government official, either directly or indirectly, or to an attorney; and (ii) solely for the purpose of reporting or investigating a suspected violation of law; or is made in a complaint or other document filed in a lawsuit or other proceeding, if such filing is made under seal. An individual who files a lawsuit for retaliation by an employer for reporting a suspected violation of law may disclose the trade secret to the attorney of the individual and use the trade secret information in the court proceeding, if the individual (i) files any document containing the trade secret under seal; and (ii) does not disclose the trade secret, except pursuant to court order. This Agreement and each party's obligations shall be binding on the representatives, assigns and successors of such party. Each party has signed this Agreement through its authorized representative.\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"DISCLOSING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" James Lahey\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"RECEIVING PARTY Signature:\"]},\" _____________________________________________________\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Typed or Printed Name:\"]},\" Richard Lafleur\"]},{\"type\":\"c\",\"name\":\"p\",\"attributes\":[],\"children\":[{\"type\":\"a\",\"name\":\"strong\",\"attributes\":[],\"children\":[\"Date:\"]},\" 10.05.2021\"]}]", "attachChange": null, "attributesBefore": {}, "attributesAfter": {} } }, "createdAt": "2023-08-24T09:37:39.710Z", "attributes": {}, "fromVersion": 14, "toVersion": 16 } ] } ``` ``` try { const response = await fetch( 'http://localhost:8000/import-document', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { documentData, suggestionAuthorId: 'user-1' } ) } ); const data = await response.json(); console.log( 'Result of importing the document:', data ); } catch ( error ) { console.log( 'Error occurred during importing the document:', error ); } ```
source file: "cs/latest/examples/collaboration-examples/storage-feature-nodejs.html" ## Document storage feature in Node.js This article presents an example of an application that uses the [document storage feature](#cs/latest/guides/collaboration/document-storage.html) in Node.js and Express.js. This example assumes that the Document Storage feature is already properly enabled. To learn how to enable document storage refer to our [dedicated guide](#cs/latest/guides/collaboration/document-storage.html--enabling-the-document-storage-feature). ### Dependencies This example uses the following dependencies: * `axios` * `body-parser` * `cors` * `express` It also uses core dependencies from Node.js: `path` and `fs`. ### Example The following example allows you to upload an [editor bundle](#cs/latest/examples/editor-bundle/editor-bundle-nodejs.html), import a document to the storage, get a list of documents, get a document, and remove a document. This file presents an example of a simple Express.js application. ``` // index.js const path = require( 'path' ); const fs = require( 'fs' ); const express = require( 'express' ); const axios = require( 'axios' ); const cors = require( 'cors' ); const bodyParser = require( 'body-parser' ); const generateSignature = require( './utils/generateSignature' ); // See: https://ckeditor.com/docs/cs/latest/examples/security/request-signature-nodejs.html. const editorBundle = fs.readFileSync( path.resolve( '../client/build/ckeditor.js' ) ); // It should be your bundled editor. const app = express(); const port = 8000; // The default application port. const apiSecret = 'SECRET'; // Do not forget to hide this value in a safe place e.g. a .env file! const organizationId = 'organizationId'; // Type your organization ID here. const environmentId = 'environmentId'; // Type your environment ID here. // If you use On-Premises application you can adjust baseApiUrl accordingly with your application URL. const baseApiUrl = `https://${ organizationId }.cke-cs.com/api/v5/${ environmentId }`; app.use( bodyParser.urlencoded( { extended: true } ) ); app.use( bodyParser.json() ); app.use( cors() ); // This function will be responsible for sending requests to CKEditor Cloud Services API. async function sendRequest( method, url, body ) { const CSTimestamp = Date.now(); const payload = { method, url, mode: 'no-cors', headers: { 'Content-Type': 'application/json', 'X-CS-Signature': generateSignature( apiSecret, method, url, CSTimestamp, body ), 'X-CS-Timestamp': CSTimestamp } }; if ( method.toUpperCase() !== 'GET' ) { payload.data = body; } try { const { status, data } = await axios( payload ); return { status, data }; } catch ( { response } ) { const { status, data } = response; return { status, data }; } } // Upload the editor bundle. Note that you will need to upload your editor again if you change the bundle. app.post( '/upload-editor', async ( req, res ) => { const { bundleVersion } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/editors`, { bundle: editorBundle.toString(), config: { cloudServices: { bundleVersion // This value should be unique per environment. } } } ); return res.json( { status, data } ); } ); // Synchronously imports the document to the storage app.post( '/storage', async ( req, res ) => { const { documentId, documentContent } = req.body; const { status, data } = await sendRequest( 'POST', `${ baseApiUrl }/storage`, { document_id: documentId, data: documentContent } ); return res.json( { status, data } ); } ); // Deletes a single document from the storage. app.delete( '/storage/:documentId', async ( req, res ) => { const { documentId } = req.params; const { status } = await sendRequest( 'DELETE', `${ baseApiUrl }/storage/${ documentId }` ); return res.json( { status } ); } ); // Gets a single document. app.get( '/storage/:documentId', async ( req, res ) => { const { documentId } = req.params; const { status, data } = await sendRequest( 'GET', `${ baseApiUrl }/storage/${ documentId }` ); return res.json( { status, data } ); } ); // Gets a list of documents. app.get( '/storage', async ( req, res ) => { const { status, data } = await sendRequest( 'GET', `${ baseApiUrl }/storage` ); return res.json( { status, data } ); } ); app.listen( port, () => console.log( `The application is listening on port ${ port }!` ) ); ``` ### Usage Run: ``` node index.js ``` By sending HTTP requests to this application you can now perform actions and communicate with CKEditor Cloud Services. See the following example: 1. Upload an editor bundle. ``` try { const response = await fetch( 'http://localhost:8000/upload-editor', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { bundleVersion: '1.0.0' } ) } ); const data = await response.json(); console.log( 'Result of uploading editor:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Synchronously import the document to the storage ``` try { const response = await fetch( 'http://localhost:8000/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { documentId: "document-1", documentContent: "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

", } ) } ); const data = await response.json(); console.log( 'Result of inserting document:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Get a list of documents – an array with the newly created `document-1` should be returned under the `data` property. ``` try { const response = await fetch( `http://localhost:8000/storage`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ); const data = await response.json(); console.log( 'Result of getting documents:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Get the document with the `document-1` ID. The content returned should be equal to the content imported in step 2. ``` try { const response = await fetch( `http://localhost:8000/storage/document-1`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ); const data = await response.json(); console.log( 'Result of getting document:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Delete the document with the `document-1` ID. ``` try { const response = await fetch( `http://localhost:8000/storage/document-1`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } } ); const data = await response.json(); console.log( 'Result of deleting document:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ``` 1. Try to get the document with the `document-1` ID again. An error will be thrown explaining that the document does not exist. ``` try { const response = await fetch( `http://localhost:8000/storage/document-1`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ); const data = await response.json(); console.log( 'Result of getting document:', data ); } catch ( error ) { console.log( 'Error occurred:', error ); } ```
source file: "cs/latest/examples/editor-bundle/advanced-editor-bundle-nodejs.html" ## Advanced editor bundling in Node.js This article presents how to create a CKEditor 5 [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) in Node.js. It is a step-by-step guide on creating an editor bundle from the CKEditor 5 package downloaded from [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/), but can be easily adapted for a variety of setups. An editor bundle is required by CKEditor Cloud Services to enable [document storage](#cs/latest/guides/collaboration/document-storage.html), [import and export](#cs/latest/guides/collaboration/import-and-export.html) as well as [connection optimization](#cs/latest/guides/collaboration/connection-optimization.html) features. ### Bundle requirements The editor bundle that can be used by CKEditor Cloud Services must fulfill certain requirements: * It needs to be built into a single `.js` file. * The plugins should be added in the [builtinPlugins](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcore%5Feditor%5Feditor-Editor.html#static-member-builtinPlugins) property to be included in the editor bundle. You cannot use the [config.plugins](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-plugins) option for adding the plugins to the editor. * The `output.library` property in the webpack configuration should be set to`CKEditorCS` value. * Plugins used in the editor bundle cannot execute external HTTP requests. * The editor instance needs to be a default export in the editor bundle. ### Creating a bundle The following example acts as a template for how to prepare an editor bundle configuration. It assumes that you have basic CKEditor 5 files - the `package.json`, `main.js`, `index.html` and `style.css` files which are the result of using [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/) with “Self-hosted (npm)” integration method. #### Dependencies This example uses the following dependencies: * `ckeditor5` * `ckeditor5-premium-features` * `webpack` * `webpack-cli` * `mini-css-extract-plugin` If you followed the Builder setup, `ckeditor5*` packages should be already there. For the rest run: ``` npm install -D webpack webpack-cli mini-css-extract-plugin ``` After that, your `package.json` dependencies sections should look as below: ``` { "dependencies": { "ckeditor5": "^42.0.0", "ckeditor5-premium-features": "^42.0.0" }, "devDependencies": { "mini-css-extract-plugin": "^2.9.0", "webpack": "^5.92.1", "webpack-cli": "^5.1.4" } } ``` #### Editor setup It is highly recommended to use the same editor setup for both creating a bundle and integrating CKEditor 5 with the frontend layer. This way, it is easier to keep everything synchronized and consistent between the front end and back end. This requires slight adjustments to the above setup in how parts of the editor are defined and used. The common parts for both setups are the editor class, list of plugins and editor config. We will split the `main.js` file into 3 files: * The `main.js` file that will define shared parts. * The `main-fe.js` file, that will be responsible for editor setup in the frontend application. * The `main-be.js` file, from which the editor bundle will be generated. It will be bundler entry file. The first step is adjusting the `main.js` file. ``` // main.js import { ClassicEditor, Essentials, Paragraph, Bold, Italic } from 'ckeditor5'; import { RealTimeCollaborativeEditing } from 'ckeditor5-premium-features'; // 1. Remove style imports. Those will be used in main-fe.js. // import 'ckeditor5/ckeditor5.css'; // import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; // import './style.css'; // 2. Create a list of plugins which will be exported. const pluginList = [ Essentials, Paragraph, Bold, Italic, RealTimeCollaborativeEditing ]; // 3. Adjust 'editorConfig' to utilize 'pluginList'. const editorConfig = { plugins: pluginList, ... } // 4. Export shared parts, instead of initializing editor. // ClassicEditor.create(document.querySelector('#editor'), editorConfig); export { ClassicEditor, pluginList, editorConfig } ``` Next, we will create `main-fe.js` which will be used on the front end. ``` // main-fe.js import { ClassicEditor, editorConfig } from './main.js'; // 1. Move style imports from main.js here. import 'ckeditor5/ckeditor5.css'; import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; import './style.css'; // 2. Initialize editor. ClassicEditor.create(document.querySelector('#editor'), editorConfig); ``` After that, `main-be.js` file, used for bundling, should be created. ``` // main-be.js import { ClassicEditor, pluginList } from './main.js'; class CKEditorCS extends ClassicEditor {} // 1. Assign plugins to be used in bundle. CKEditorCS.builtinPlugins = pluginList; // 2. Export editor class. export default CKEditorCS; ``` #### Frontend building With the above changes, your application can be build and bundled the same way as before. The only adjustment needed is changing the file which is imported. ``` ... ... ``` #### Bundler configuration Next, you need to prepare [webpack](https://webpack.js.org/) configuration to generate a valid bundle. ``` const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); module.exports = { entry: './main-be.js', // Your editor build configuration. output: { filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services. library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name! libraryTarget: 'umd', libraryExport: 'default', clean: true }, plugins: [ new MiniCssExtractPlugin() ], module: { parser: { javascript: { dynamicImportMode: 'eager' } }, rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader'] } ] }, performance: { hints: false } }; ``` #### Generating the bundle Then, run the following commands inside the CKEditor 5 package folder: ``` npx webpack --mode production ``` This command will result in the generation of the `./dist/editor.bundle.js` file. This is the bundle file which then should be [uploaded to CKEditor Cloud Services server](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle). #### Building the editor bundle with TypeScript The examples presented above are using JavaScript in the editor source files. Building the editor bundle with TypeScript is also possible. To start using TypeScript, follow these steps: 1. Integrate TypeScript with webpack. Refer to [this guide](https://webpack.js.org/guides/typescript/) for more details. 2. Change the extension of the editor source file to `.ts`. Adjust the entry path in `webpack.config.js`. 3. Refer to the [Working with TypeScript](https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/typescript-support.html) guide and adjust the imports and configuration settings if needed. ### Creating a bundle from legacy configuration The following example acts as a template for how to prepare an editor build and bundler configuration. It assumes that you have an existing CKEditor 5 package with `ckeditor.js`, `package.json` and `webpack.config.js` files. #### Dependencies This example uses the following dependencies: * `@ckeditor/ckeditor5-editor-classic` * `@ckeditor/ckeditor5-basic-styles` * `@ckeditor/ckeditor5-essentials` * `@ckeditor/ckeditor5-paragraph` * `@ckeditor/ckeditor5-real-time-collaboration` * `@ckeditor/ckeditor5-dev-utils` * `@ckeditor/ckeditor5-theme-lark` * `webpack` * `webpack-cli` * `postcss-loader` * `raw-loader` * `style-loader` The `package.json` file dependencies sections should look similar to one below. ``` { "dependencies": { "@ckeditor/ckeditor5-basic-styles": "41.4.2", "@ckeditor/ckeditor5-editor-classic": "41.4.2", "@ckeditor/ckeditor5-essentials": "41.4.2", "@ckeditor/ckeditor5-paragraph": "41.4.2", "@ckeditor/ckeditor5-real-time-collaboration": "41.4.2" }, "devDependencies": { "@ckeditor/ckeditor5-dev-utils": "^32.1.2", "@ckeditor/ckeditor5-theme-lark": "41.4.2", "postcss-loader": "^4.3.0", "raw-loader": "^4.0.2", "style-loader": "^2.0.0", "webpack": "^5.91.0", "webpack-cli": "^4.10.0" } } ``` #### Editor build configuration This file presents an example of an editor build configuration. It is used by the bundler as an entry file. This `ckeditorcs.js` file should be created based on your `ckeditor.js` file. ``` // ckeditorcs.js // The editor base creator to use. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; // All plugins that you would like to use in your editor. import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import RealTimeCollaborativeEditing from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativeediting'; class CKEditorCS extends ClassicEditor {} // Load all plugins you would like to use in your editor this way. // This is the only way to load plugins into the editor which will then be used in CKEditor Cloud Services. CKEditorCS.builtinPlugins = [ Essentials, Paragraph, Bold, Italic, RealTimeCollaborativeEditing ]; // Export your editor. export default CKEditorCS; ``` #### Bundler configuration This file presents an example of the bundler configuration. Here [webpack](https://webpack.js.org/) is used as the bundler. ``` // webpack.config.js const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); module.exports = { entry: './ckeditorcs.js', // Your editor build configuration. output: { filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services. library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name! libraryTarget: 'umd', libraryExport: 'default' }, module: { parser: { javascript: { dynamicImportMode: 'eager' } }, rules: [ { test: /\.svg$/, use: [ 'raw-loader' ] }, { test: /\.css$/, use: [ { loader: 'style-loader', options: { injectType: 'singletonStyleTag' } }, { loader: 'postcss-loader', options: styles.getPostCssConfig( { themeImporter: { themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) }, minify: true } ) } ] } ] }, performance: { hints: false } }; ``` The most important part here is `output.library` field. Without this, the bundle will not work with the CKEditor Cloud Services server. ``` module.exports = { // ... output: { library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name! // ... }, // ... }; ``` #### Generating the bundle Run the following commands inside the CKEditor 5 package folder: ``` npm install npx webpack --mode production ``` This command will result in the generation of the `./dist/editor.bundle.js` file. This is the bundle file which then should be [uploaded to the CKEditor Cloud Services server](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle). #### Editor bundle with watchdog, context or “super builds” Cloud Services expects the `Editor` class as the default export in the bundled file. If you are using an editor bundle with [Watchdog](#ckeditor5/latest/features/watchdog.html) and [context](#ckeditor5/latest/features/collaboration/context-and-collaboration-features.html) or you are building multiple editors as [“super build”](https://ckeditor.com/docs/ckeditor5/latest/getting-started/legacy/advanced/using-two-editors.html#creating-super-builds), you need to take extra steps to be able to upload these editors. You can build multiple editor bundles from multiple source files for different purposes. A single webpack build can be then used to bundle all of them. Thanks to this approach, you will have an editor bundle that is compatible with the Cloud Services and you can still use the other editor bundle and take advantage of the “super builds”, watchdog, or context. ##### Creating a legacy editor bundle with Watchdog Assuming that you have a `ckeditor.js` source file that is exporting the editor with watchdog: ``` import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog'; // Other plugins imports. class Editor extends ClassicEditor {} Editor.builtinPlugins = [ // Imported plugins. ]; const watchdog = new EditorWatchdog( Editor ); export default watchdog; ``` You can now create the `ckeditorcs.js` source file with the same content as the above file. The only difference is the `export` in this file. Instead of exporting the watchdog, you should export the `Editor` instance: `export default Editor;`. With the two source files, you can tweak the webpack config to bundle both editors in a single build step: ``` 'use strict'; const path = require( 'path' ); const webpack = require( 'webpack' ); const { bundler, styles } = require( '@ckeditor/ckeditor5-dev-utils' ); const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' ); const TerserWebpackPlugin = require( 'terser-webpack-plugin' ); const config = { devtool: 'source-map', performance: { hints: false }, optimization: { minimizer: [ new TerserWebpackPlugin( { sourceMap: true, terserOptions: { output: { comments: /^!/ } }, extractComments: false } ) ] }, plugins: [ new CKEditorTranslationsPlugin( { language: 'en', additionalLanguages: 'all' } ), new webpack.BannerPlugin( { banner: bundler.getLicenseBanner(), raw: true } ) ], module: { parser: { javascript: { dynamicImportMode: 'eager' } }, rules: [ { test: /\.svg$/, use: [ 'raw-loader' ] }, { test: /\.css$/, use: [ { loader: 'style-loader', options: { injectType: 'singletonStyleTag', attributes: { 'data-cke': true } } }, { loader: 'css-loader' }, { loader: 'postcss-loader', options: { postcssOptions: styles.getPostCssConfig( { themeImporter: { themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) }, minify: true } ) } } ] } ] } }; module.exports = [ // The first bundle will have the editor with watchdog and can be used in your application. { ...config, entry: path.resolve( __dirname, 'src', 'ckeditor.js' ), output: { library: 'Watchdog', path: path.resolve( __dirname, 'build' ), filename: 'ckeditor.js', libraryTarget: 'umd', libraryExport: 'default' } }, // The second bundle will be ready to be uploaded to the Cloud Services server. { ...config, entry: path.resolve( __dirname, 'src', 'ckeditorcs.js' ), output: { library: 'CKEditorCS', path: path.resolve( __dirname, 'build' ), filename: 'ckeditorcs.js', libraryTarget: 'umd', libraryExport: 'default' } } ]; ``` Similarly, you can build multiple bundles if you are using context or “super builds”. In the case of a bundle with context make sure that the bundle for Cloud Services includes all the plugins that the context editor has. For super builds, you can simply create a copy of a source file and export a single editor instance that you would like to upload to Cloud Services. #### Building the editor bundle with TypeScript The examples presented above are using JavaScript in the editor source files. Building the editor bundle with TypeScript is also possible. To start using TypeScript, follow these steps: 1. Integrate TypeScript with webpack. Refer to [this guide](https://webpack.js.org/guides/typescript/) for more details. 2. Change the extension of the editor source file to `.ts`. Adjust the entry path in `webpack.config.js`. 3. Refer to the [Working with TypeScript](https://ckeditor.com/docs/ckeditor5/latest/getting-started/legacy/working-with-typescript.html) guide and adjust the imports and configuration settings if needed. source file: "cs/latest/examples/editor-bundle/editor-bundle-nodejs.html" ## Creating an editor bundle in Node.js This article presents examples of a CKEditor 5 [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) in Node.js. An editor bundle is required by CKEditor Cloud Services to enable the [document storage](#cs/latest/guides/collaboration/document-storage.html), [import and export](#cs/latest/guides/collaboration/import-and-export.html) as well as [connection optimization](#cs/latest/guides/collaboration/connection-optimization.html) features. ### Example bundle The following example presents a minimalistic editor bundle setup. It is based on 3 files - `package.json`, `ckeditorcs.js` and `webpack.config.js`, and is ready to use. #### Dependencies This example uses the following dependencies: * `ckeditor5` * `ckeditor5-premium-features` * `webpack` * `webpack-cli` * `mini-css-extract-plugin` With the above, the `package.json` file should look like the one below. ``` { "name": "ckeditorcs-bundle", "version": "1.0.0", "private": true, "dependencies": { "ckeditor5": "^42.0.0", "ckeditor5-premium-features": "^42.0.0" }, "devDependencies": { "webpack": "^5.91.0", "webpack-cli": "^4.10.0", "mini-css-extract-plugin": "^2.9.0" } } ``` #### Editor build configuration The `ckeditorcs.js` file presents an example of an editor build configuration. It is used by the bundler as an entry file. ``` import { // The editor base creator to use. ClassicEditor, // All plugins that you would like to use in your editor. Essentials, Paragraph, Bold, Italic } from 'ckeditor5'; import { // All premium plugins that you would like to use in your editor. RealTimeCollaborativeEditing } from 'ckeditor5-premium-features'; class CKEditorCS extends ClassicEditor {} // Load all plugins you would like to use in your editor this way. // This is the only way to load plugins into the editor which will then be used in CKEditor Cloud Services. CKEditorCS.builtinPlugins = [ Essentials, Paragraph, Bold, Italic, RealTimeCollaborativeEditing ]; // Export your editor. export default CKEditorCS; ``` #### Bundler configuration The `webpack.config.js` file presents the bundler configuration. Here [webpack](https://webpack.js.org/) is used as the bundler. ``` const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); module.exports = { entry: './ckeditorcs.js', // Your editor build configuration. output: { filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services. library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name! libraryTarget: 'umd', libraryExport: 'default', clean: true }, plugins: [ new MiniCssExtractPlugin() ], module: { parser: { javascript: { dynamicImportMode: 'eager' } }, rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader'] } ] }, performance: { hints: false } }; ``` #### Generating the bundle Run the following commands inside the folder with these files: ``` npm install npx webpack --mode production ``` This command will result in the generation of the `./dist/editor.bundle.js` file. This is the bundle file which then should be [uploaded to CKEditor Cloud Services server](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle). ### Example bundle (legacy configuration) The following example presents a minimalistic editor bundle setup. It is based on 3 files - `package.json`, `ckeditorcs.js` and `webpack.config.js`, and is ready to use. #### Dependencies This example uses the following dependencies: * `@ckeditor/ckeditor5-editor-classic` * `@ckeditor/ckeditor5-basic-styles` * `@ckeditor/ckeditor5-essentials` * `@ckeditor/ckeditor5-paragraph` * `@ckeditor/ckeditor5-real-time-collaboration` * `@ckeditor/ckeditor5-dev-utils` * `@ckeditor/ckeditor5-theme-lark` * `css-loader` * `postcss-loader` * `raw-loader` * `style-loader` * `webpack` * `webpack-cli` With the above, the `package.json` file should look like the one below: ``` { "name": "ckeditorcs-bundle", "version": "1.0.0", "private": true, "dependencies": { "@ckeditor/ckeditor5-editor-classic": "41.4.2", "@ckeditor/ckeditor5-basic-styles": "41.4.2", "@ckeditor/ckeditor5-essentials": "41.4.2", "@ckeditor/ckeditor5-paragraph": "41.4.2", "@ckeditor/ckeditor5-real-time-collaboration": "41.4.2" }, "devDependencies": { "@ckeditor/ckeditor5-dev-utils": "^40.2.2", "@ckeditor/ckeditor5-theme-lark": "41.4.2", "css-loader": "^5.2.7", "postcss-loader": "^4.3.0", "raw-loader": "^4.0.2", "style-loader": "^2.0.0", "webpack": "^5.91.0", "webpack-cli": "^4.10.0" } } ``` #### Editor build configuration The `ckeditorcs.js` file presents an example of an editor build configuration. It is used by the bundler as an entry file. ``` // The editor base creator to use. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; // All plugins that you would like to use in your editor. import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import RealTimeCollaborativeEditing from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativeediting'; class CKEditorCS extends ClassicEditor {} // Load all plugins you would like to use in your editor this way. // This is the only way to load plugins into the editor which will then be used in CKEditor Cloud Services. CKEditorCS.builtinPlugins = [ Essentials, Paragraph, Bold, Italic, RealTimeCollaborativeEditing ]; // Export your editor. export default CKEditorCS; ``` #### Bundler configuration The `webpack.config.js` file presents an example of the bundler configuration. Here [webpack](https://webpack.js.org/) is used as the bundler. ``` const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); module.exports = { entry: './ckeditorcs.js', // Your editor build configuration. output: { filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services. library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name! libraryTarget: 'umd', libraryExport: 'default' }, module: { parser: { javascript: { dynamicImportMode: 'eager' } }, rules: [ { test: /\.svg$/, use: [ 'raw-loader' ] }, { test: /\.css$/, use: [ { loader: 'style-loader', options: { injectType: 'singletonStyleTag' } }, { loader: 'css-loader' }, { loader: 'postcss-loader', options: { postcssOptions: styles.getPostCssConfig( { themeImporter: { themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) }, minify: true } ) } } ] } ] }, performance: { hints: false } }; ``` #### Generating the bundle Run the following commands inside the CKEditor 5 package folder: ``` npm install npx webpack --mode production ``` This command will result in the generation of the `./dist/editor.bundle.js` file. This is the bundle file which then should be [uploaded to CKEditor Cloud Services server](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle). source file: "cs/latest/examples/index.html" ## Cloud Services examples This section provides examples on how to implement the following: * **Token endpoints** – Learn how to create a token endpoint in {% raw %}ASP.NET{% endraw %}, Node.js and PHP. * **Webhooks** – Learn how to create a webhooks server. * **Security** – Learn how to create a request signature. * **On-Premises** – Learn how to create scripts to manage on-premises. * **Editor bundle** – Learn how to create an editor bundle for the document storage as well as import and export features. * **Collaboration** – Learn how to create an application that uses the document import and export feature. * **Server-side Editor API** – Learn how to execute JavaScript code on the server to manipulate document content and collaborative data. source file: "cs/latest/examples/on-premises/creating-environment-nodejs.html" ## Creating an environment in Node.js This article presents an example of using the `Cloud Services Management REST API` to create an environment in Node.js. ### Dependencies This example uses `request-promise-native` and core dependencies from Node.js: `crypto`, `url`. ### Example The following simple example presents how to create an environment using the `Cloud Services Management REST API`. The `ENVIRONMENTS_MANAGEMENT_SECRET_KEY` and `APPLICATION_ENDPOINT` variables should be set to proper values from the configuration. ``` const url = require( 'url' ); const crypto = require( 'crypto' ); const requestPromise = require( 'request-promise-native' ); const ENVIRONMENTS_MANAGEMENT_SECRET_KEY = 'secret'; const APPLICATION_ENDPOINT = 'http://localhost:8000'; ( async () => { try { const newEnvironment = { id: _randomString( 20 ), // required length 20 name: 'Production', organizationId: _randomString( 60 ), // required length 10-60 accessKeys: [ { value: _randomString( 100 ) // required length 10-120 } ], services: [ { id: _randomString( 24 ), // required length 24 type: 'easy-image' }, { id: _randomString( 24 ), // required length 24 type: 'collaboration' } ] // all these services types recommended }; const timestamp = Date.now(); const uri = `${ APPLICATION_ENDPOINT }/environments`; const method = 'POST'; const signature = _generateSignature( ENVIRONMENTS_MANAGEMENT_SECRET_KEY, method, uri, timestamp, newEnvironment ); const options = { uri, method, headers: { 'X-CS-Signature': signature, 'X-CS-Timestamp': timestamp }, body: newEnvironment, json: true, rejectUnauthorized: false // required for domains with self signed certificate }; await requestPromise( options ); console.log( 'New Environment created.' ); console.log( `EnvironmentId: ${ newEnvironment.id } AccessKey: ${ newEnvironment.accessKeys[ 0 ].value }` ); } catch ( error ) { console.log( 'error:', error.message ); } } )(); function _generateSignature( apiSecret, method, uri, timestamp, body ) { const path = url.parse( uri ).path; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); } return hmac.digest( 'hex' ); } function _randomString( length ) { return crypto.randomBytes( length / 2 ).toString( 'hex' ); } ``` ### Usage Run: ``` node index.js ``` source file: "cs/latest/examples/security/request-signature-dotnet.html" ## Request signature in ASP.NET This article presents a sample implementation of a [request signature](#cs/latest/developer-resources/security/request-signature.html) in ASP.NET. ### Dependencies This example uses `System` dependencies from ASP.NET. ### Example The following simple example implements the algorithm described in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. The most important thing is to use the `HMACSHA256` from `System.Security.Cryptography` and pass the parameters in the correct order: `method`, `url`, `timestamp`, `body`. The `method` parameter should be uppercase and `path` should contain only the relative path and query parameters from the URL, not the full URL address. The full URL address should be converted to e.g. `/webhook?a=1`. If the algorithm works correctly, it should generate the same signature as the one given below: `56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762` for the following parameters: * `apiSecret=SECRET` * `method=POST` * `uri=http://demo.example.com/webhook?a=1` * `timestamp=1563276169752` * `body={a:1}` ``` using System; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Security.Cryptography; public class Program { public static void Main() { string signature = GenerateSignature( "SECRET", "POST", "http://demo.example.com/webhook?a=1", 1563276169752, new { a = 1 } ); Console.WriteLine( signature == "56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762" ); } public static string GenerateSignature( string apiSecret, string method, string uri, long timestampMilliseconds, object body = null ) { string upperMethod = method.ToUpper(); Uri url = new Uri(uri); string path = url.AbsolutePath + url.Query; string message = upperMethod + path + timestampMilliseconds; if (body != null && (upperMethod == "POST" || upperMethod == "PUT")) { string bodyString = JsonSerializer.Serialize(body); message += bodyString; } return HmacSha256Digest(message, apiSecret); } public static string HmacSha256Digest(string message, string secret) { UTF8Encoding utf8Encoder = new UTF8Encoding(); byte[] encodedSecret = utf8Encoder.GetBytes(secret); byte[] encodedMessage = utf8Encoder.GetBytes(message); HMACSHA256 hmac256 = new HMACSHA256(encodedSecret); byte[] messageHash = hmac256.ComputeHash(encodedMessage); return BitConverter.ToString(messageHash).Replace("-", "").ToLower(); } } ``` source file: "cs/latest/examples/security/request-signature-java.html" ## Request signature in Java This article presents a sample implementation of a [request signature](#cs/latest/developer-resources/security/request-signature.html) in Java. ### Dependencies This example uses the [util](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/package-summary.html), [net](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/net/package-summary.html) and [crypto](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/javax/crypto/package-summary.html) dependencies from the Java SDK. ### Example The following simple example implements the algorithm described in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. The most important thing is to calculate `HMAC-SHA-256` using any library using the given parameters in the correct order: `method`, `url`, `timestamp`, `body`. The `method` parameter should be uppercase and `path` should contain only the relative path and query parameters from the URL, not the full URL address. The full URL address should be converted to e.g. `/webhook?a=1`. If the algorithm works correctly, it should generate the same signature as the one given below: `56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762` for the following parameters: * `apiSecret=SECRET` * `method=POST` * `uri=http://demo.example.com/webhook?a=1` * `timestamp=1563276169752` * `body={a:1}` ``` import java.util.Formatter; import java.util.Map; import java.net.URI; import javax.crypto.spec.SecretKeySpec; import javax.crypto.Mac; public class Main { private static final String HMAC_SHA256 = "HmacSHA256"; public static void main(String args[]) throws Exception { String expectedSignature = "56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762"; String signature = generateSignature( "SECRET", "POST", "http://demo.example.com/webhook?a=1", 1563276169752l, "{\"a\":1}" ); System.out.println(signature.equals(expectedSignature)); } private static String toHexString(byte[] bytes) { Formatter formatter = new Formatter(); for (byte b: bytes) { formatter.format("%02x", b); } return formatter.toString(); } public static String calculateHMACSHA256(String data, String key) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), HMAC_SHA256); Mac mac = Mac.getInstance(HMAC_SHA256); mac.init(secretKeySpec); return toHexString(mac.doFinal(data.getBytes())); } private static String generateSignature(String secret, String method, String uri, long timestamp) throws Exception { return generateSignature(secret, method, uri, timestamp, ""); } private static String generateSignature(String secret, String method, String uri, long timestamp, String bodyString) throws Exception { String methodUpperCase = method.toUpperCase(); URI url = new URI(uri); String path = url.getPath(); String query = url.getQuery(); if (!query.isBlank()) { path = path + "?" + query; } String signatureData = methodUpperCase + path + timestamp; if (bodyString != null) { signatureData += bodyString; } return calculateHMACSHA256(signatureData, secret); } } ``` source file: "cs/latest/examples/security/request-signature-nodejs.html" ## Request signature in Node.js This article presents a sample implementation of a [request signature](#cs/latest/developer-resources/security/request-signature.html) in Node.js. ### Dependencies This example uses only core dependecies from Node.js: `crypto` and `url`. ### Example The following simple example implements the algorithm described in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. The most important thing is to use the `crypto` module with the appropriate `SHA256` algorithm and give the parameters in the right order: `method`, `url`, `timestamp`, `body`. The `method` parameter should be provided in uppercase and the `url` should contain only the path from the URL, not the full URL address. The full URL address should be converted to `/webhook?a=1`. If the algorithm works correctly, it should generate the same signature as the one given below: `56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762` for the following parameters: * `apiSecret=SECRET` * `method=POST` * `uri=http://demo.example.com/webhook?a=1` * `timestamp=1563276169752` * `body={a:1}` ``` const crypto = require( 'crypto' ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); } return hmac.digest( 'hex' ); } const expectedSignature = '56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762'; const generatedSignature = generateSignature( 'SECRET', 'POST', 'http://demo.example.com/webhook?a=1', 1563276169752, { a: 1 } ); console.log( expectedSignature === generatedSignature ); ``` ### Usage Run: ``` node index.js ``` source file: "cs/latest/examples/security/request-signature-php.html" ## Request signature in PHP This article presents a sample implementation of a [request signature](#cs/latest/developer-resources/security/request-signature.html) in PHP. ### Dependencies No external dependencies are required to generate the signature. ### Example The following simple example implements the algorithm described in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. The most important thing is to use the `hash_hmac` function with the appropriate `sha256` algorithm and give the parameters in the right order: `method`, `url`, `timestamp`, `body`. The `method` parameter should be provided in uppercase and the `uri` should contain only the path from the URL, not the full URL address. The full URL address should be converted to `/webhook?a=1`. If the algorithm works correctly, it should generate the same signature as the one given below: `56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762` for the following parameters: * `apiSecret=SECRET` * `method=POST` * `uri=http://demo.example.com/webhook?a=1` * `timestamp=1563276169752` * `body=['a' => 1]` ``` 1] ); echo $expectedSignature === $generatedSignature ? 'true' : 'false'; ``` ### Usage Run: ``` php index.php ``` The above code should print `true` in the console. source file: "cs/latest/examples/security/request-signature-python.html" ## Request signature in Python 3 This article presents a sample implementation of a [request signature](#cs/latest/developer-resources/security/request-signature.html) in Python 3. ### Dependencies This example uses the [hmac](https://docs.python.org/3/library/hmac.html), [hashlib](https://docs.python.org/3/library/hashlib.html), [json](https://docs.python.org/3/library/json.html) and [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) core dependencies from Python 3. ### Example The following simple example implements the algorithm described in the [Request signature](#cs/latest/developer-resources/security/request-signature.html) guide. The most important thing is to use the `hmac` module with the appropriate `SHA256` algorithm and provide the parameters in the correct order: `method`, `url`, `timestamp`, `body`. The `method` parameter should be uppercase and `path` should contain only the relative path and query parameters from the URL, not the full URL address. The full URL address should be converted to e.g. `/webhook?a=1`. If the algorithm works correctly, it should generate the same signature as the one given below: `56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762` for the following parameters: * `apiSecret=SECRET` * `method=POST` * `uri=http://demo.example.com/webhook?a=1` * `timestamp=1563276169752` * `body={a:1}` ``` import hmac import hashlib import json import urllib.parse def hmacDigest(data, key): keyEncoded = key.encode() dataEncoded = data.encode() h = hmac.new(keyEncoded, dataEncoded, hashlib.sha256) return h.hexdigest() def generateSignature(apiSecret, method, uri, timestamp, body): url = urllib.parse.urlparse(uri) path = url.path if (url.query): path = path + "?" + url.query methodUpperCase = method.upper() data = methodUpperCase + path + str(timestamp) if (body): data += json.dumps(body, separators=(',',':'), ensure_ascii=False) return hmacDigest(data, apiSecret) expectedSignature = "56ac656c7f932c5b775be28949e90af9a2356eae2826539f10ab6526a0eec762" generatedSignature = generateSignature( "SECRET", "POST", "http://demo.example.com/webhook?a=1", 1563276169752, {"a": 1} ) print(generatedSignature == expectedSignature) ``` ### Usage Run: ``` python3 index.py ``` source file: "cs/latest/examples/server-side-editor-api-examples/extract-document-content-in-nodejs.html" ## Extract document content in Node.js This article presents an example of an application that allows for extracting document content using the [Server-side Editor API](#cs/latest/developer-resources/server-side-editor-api/editor-scripts.html) feature. ### Dependencies This example uses the following dependencies: * `axios` It also uses core dependencies from Node.js: `crypto`. ### Example This example demonstrates how to use the Server-side Editor API to extract document content using Node.js. Make sure you have Node.js and npm installed before following these steps. 1. Create a new project and install dependencies: ``` mkdir cs-sse-api-example && cd cs-sse-api-example && npm init -y && npm i axios && touch sse-api.js ``` 1. Open `cs-sse-api-example/sse-api.js` and paste the following code snippet: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Set document id const documentId = 'my_document_id'; // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; const apiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/collaborations/${ documentId }/evaluate-script`; // Example script that extracts document content const script = ` const data = editor.getData(); return { content: data, wordCount: data.split(' ').length, characterCount: data.length }; `; const body = { 'script': script, 'user': { 'id': 'txQ9sTfqmXUyWU5LmDbr', 'name': 'System Process', } }; const CSTimestamp = Date.now(); const config = { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, 'POST', apiEndpoint, CSTimestamp, body ) }, }; axios.post( apiEndpoint, body, config ) .then( response => { console.log( 'Status:', response.status ); console.log( 'Response data:', response.data ); } ).catch( error => { console.log( 'Error:', error.message ); console.log( 'Response data:', error.response.data ); } ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); }; return hmac.digest( 'hex' ); } ``` 1. Update your credentials and the `documentId` values in the code snippet. 2. Execute the script: ``` node sse-api.js ``` After a successful response with a status code `201`, you should see the document content, word count, and character count in the console output. source file: "cs/latest/examples/token-endpoints/dotnet.html" ## Token endpoint in ASP.NET This article presents a simple [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) example for creating JSON Web Tokens (JWT) implemented in ASP.NET. The tokens are used by CKEditor Cloud Services to authenticate users. ### Dependencies All code examples use the [System.IdentityModel.Tokens.Jwt](https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/) library. If you are using the [package manager console](https://docs.microsoft.com/en-us/nuget/tools/package-manager-console) in Visual Studio, you can run the following: ``` Install-Package System.IdentityModel.Tokens.Jwt ``` ### Examples When creating a token endpoint to integrate with [Collaboration](#cs/latest/guides/collaboration/quick-start.html), the token payload should contain the environment ID and user data. #### Real-time collaboration features ``` using System; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; using System.Collections.Generic; namespace CSTokenExample { class Program { static string createCSToken(string environmentId, string accessKey) { var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(accessKey)); var signingCredentials = new SigningCredentials(securityKey, "HS256"); var header = new JwtHeader(signingCredentials); var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow); var payload = new JwtPayload { { "aud", environmentId }, { "iat", dateTimeOffset.ToUnixTimeSeconds() }, { "sub", "user-123" }, { "user", new Dictionary { { "email", "joe.doe@example.com" }, { "name", "Joe Doe" } } }, { "auth", new Dictionary { { "collaboration", new Dictionary { { "*", new Dictionary { { "role", "writer" } } } } } } } }; var securityToken = new JwtSecurityToken(header, payload); var handler = new JwtSecurityTokenHandler(); return handler.WriteToken(securityToken); } static void Main(string[] args) { string accessKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; string environmentId = "LJRQ1bju55p6a47RwadH"; var tokenString = createCSToken(environmentId, accessKey); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. Console.WriteLine(tokenString); } } } ``` `accessKey` and `environmentId` should be replaced with the keys provided by the [Customer Portal](https://portal.ckeditor.com/) for SaaS or by the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. User data can be taken from the session or the database. You should then pass the token to the client in an HTTP response of the token endpoint. Do not forget to authenticate the user in your application before you send the token. If the user is unauthenticated, the token endpoint should return an error or redirect to the login page. You should also make sure the token is sent via an encrypted channel. #### CKEditor AI When creating a token endpoint for [CKEditor AI](#cs/latest/guides/ckeditor-ai/overview.html), the token payload should include AI-specific permissions in the `auth.ai.permissions` array. For a full list of available permissions, see the [Permissions guide](#cs/latest/guides/ckeditor-ai/permissions.html). ``` using System; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; using System.Collections.Generic; namespace CSTokenExample { class Program { static string createCSToken(string environmentId, string accessKey) { var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(accessKey)); var signingCredentials = new SigningCredentials(securityKey, "HS256"); var header = new JwtHeader(signingCredentials); var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow); var payload = new JwtPayload { { "aud", environmentId }, { "iat", dateTimeOffset.ToUnixTimeSeconds() }, { "sub", "user-123" }, { "user", new Dictionary { { "email", "joe.doe@example.com" }, { "name", "Joe Doe" } } }, { "auth", new Dictionary { { "ai", new Dictionary { { "permissions", new List { "ai:conversations:*", "ai:models:agent", "ai:actions:system:*", "ai:reviews:system:*" } } } } } } }; var securityToken = new JwtSecurityToken(header, payload); var handler = new JwtSecurityTokenHandler(); return handler.WriteToken(securityToken); } static void Main(string[] args) { string accessKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; string environmentId = "LJRQ1bju55p6a47RwadH"; var tokenString = createCSToken(environmentId, accessKey); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. Console.WriteLine(tokenString); } } } ``` If you use both collaboration and AI, combine both `auth.collaboration` and `auth.ai` in a single token payload. #### Easy Image, Export to PDF and Import and Export to Word The token endpoint for Easy Image and the Export to Word/PDF features does not require adding user data. You can therefore skip the `user` and `auth` properties in the token payload. #### Export to PDF and Import and Export to Word On-Premises Tokens for PDF Converter and DOCX Converter On-Premises do not require any additional claims, so you can create the token with an empty payload. In this implementation, `accessKey` has been replaced by `SECRET_KEY` \- a variable set during the [Import and Export to Word](#cs/latest/onpremises/docx-onpremises/installation.html)/[Export to PDF](#cs/latest/onpremises/pdf-onpremises/installation.html) On-Premises instance installation. ``` using System; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; using System.Collections.Generic; namespace CSTokenExample { class Program { static string createCSToken(string secretKey) { var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); var signingCredentials = new SigningCredentials(securityKey, "HS256"); var header = new JwtHeader(signingCredentials); var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow); var payload = new JwtPayload { { "iat", dateTimeOffset.ToUnixTimeSeconds() } }; var securityToken = new JwtSecurityToken(header, payload); var handler = new JwtSecurityTokenHandler(); return handler.WriteToken(securityToken); } static void Main(string[] args) { string secretKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; var tokenString = createCSToken(secretKey); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. Console.WriteLine(tokenString); } } } ``` If you create your own token endpoint, do not forget to authenticate the user before you send the token. ### Example response The result should be in a plain text format. ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJMSlJRMWJqdTU1cDZhNDdSd2FkSCIsImlhdCI6MTY0OTIyOTQyMiwic3ViIjoidXNlci0xMjMiLCJ1c2VyIjp7ImVtYWlsIjoiam9lLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2UgRG9lIn0sImF1dGgiOnsiY29sbGFib3JhdGlvbiI6eyIqIjp7InJvbGUiOiJ3cml0ZXIifX19fQ._V-HXKKHU1E-saZxk4JTvgXdh1I7793nCEK91ubSZHY ``` ### Debugging For debugging purposes [jwt.io](https://jwt.io) can be used. source file: "cs/latest/examples/token-endpoints/java.html" ## Token endpoint in Java This article presents a simple [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) example for creating JSON Web Tokens (JWT) implemented in Java. The tokens are used by CKEditor Cloud Services to authenticate users. ### Dependencies All examples use the [jjwt](https://github.com/jwtk/jjwt) library for creating tokens. For installation instructions check the [official guide](https://github.com/jwtk/jjwt#install). ### Examples When creating a token endpoint to integrate with [Collaboration](#cs/latest/guides/collaboration/quick-start.html), the token payload should contain the environment ID and user data. #### Real-time collaboration features ``` import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.security.Key; public class Main { private static String createCSToken(String accessKey, String environmentId) throws UnsupportedEncodingException { Key key = Keys.hmacShaKeyFor(accessKey.getBytes("ASCII")); Map payload = new HashMap<>() {{ put("aud", environmentId); put("iat", System.currentTimeMillis() / 1000); put("sub", "user-123"); put("user", new HashMap<>() {{ put("email", "joe.doe@example.com"); put("name", "Joe Doe"); }}); put("auth", new HashMap<>() {{ put("collaboration", new HashMap<>() {{ put("*", new HashMap<>() {{ put("role", "writer"); }}); }}); }}); }}; return Jwts.builder().addClaims(payload).signWith(key).compact(); } public static void main(String[] args) { String accessKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; String environmentId = "LJRQ1bju55p6a47RwadH"; try { var tokenString = createCSToken(accessKey, environmentId); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. System.out.println(tokenString); } catch (Exception exception) { System.out.println(exception); } } } ``` `accessKey` and `environmentId` should be replaced with the keys provided by the [Customer Portal](https://portal.ckeditor.com/) for SaaS or by the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. User data can be taken from the session or the database. You should then pass the token to the client in an HTTP response of the token endpoint. Do not forget to authenticate the user in your application before you send the token. If the user is unauthenticated, the token endpoint should return an error or redirect to the login page. You should also make sure the token is sent via an encrypted channel. #### CKEditor AI When creating a token endpoint for [CKEditor AI](#cs/latest/guides/ckeditor-ai/overview.html), the token payload should include AI-specific permissions in the `auth.ai.permissions` array. For a full list of available permissions, see the [Permissions guide](#cs/latest/guides/ckeditor-ai/permissions.html). ``` import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.security.Key; public class Main { private static String createCSToken(String accessKey, String environmentId) throws UnsupportedEncodingException { Key key = Keys.hmacShaKeyFor(accessKey.getBytes("ASCII")); Map payload = new HashMap<>() {{ put("aud", environmentId); put("iat", System.currentTimeMillis() / 1000); put("sub", "user-123"); put("user", new HashMap<>() {{ put("email", "joe.doe@example.com"); put("name", "Joe Doe"); }}); put("auth", new HashMap<>() {{ put("ai", new HashMap<>() {{ put("permissions", Arrays.asList( "ai:conversations:*", "ai:models:agent", "ai:actions:system:*", "ai:reviews:system:*" )); }}); }}); }}; return Jwts.builder().addClaims(payload).signWith(key).compact(); } public static void main(String[] args) { String accessKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; String environmentId = "LJRQ1bju55p6a47RwadH"; try { var tokenString = createCSToken(accessKey, environmentId); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. System.out.println(tokenString); } catch (Exception exception) { System.out.println(exception); } } } ``` If you use both collaboration and AI, combine both `auth.collaboration` and `auth.ai` in a single token payload. #### Easy Image, Export to PDF and Import and Export to Word The token endpoint for Easy Image and the Export to Word/PDF features does not require adding user data. You can therefore skip the `user` and `auth` properties in the token payload. #### Export to PDF and Import and Export to Word On-Premises Tokens for PDF Converter and DOCX Converter On-Premises do not require any additional claims, so you can create the token with an empty payload. In this implementation, `accessKey` has been replaced by `SECRET_KEY` \- a variable set during the [Import and Export to Word](#cs/latest/onpremises/docx-onpremises/installation.html)/[Export to PDF](#cs/latest/onpremises/pdf-onpremises/installation.html) On-Premises instance installation. ``` import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.io.UnsupportedEncodingException; import java.util.Date; import java.security.Key; public class Main { private static String createCSToken(String secretKey) throws UnsupportedEncodingException { Key key = Keys.hmacShaKeyFor(secretKey.getBytes("ASCII")); return Jwts.builder().setIssuedAt(new Date()).signWith(key).compact(); } public static void main(String[] args) { String secretKey = "w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy"; try { var tokenString = createCSToken(secretKey); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. System.out.println(tokenString); } catch (Exception exception) { System.out.println(exception); } } } ``` ### Example response The result should be in a plain text format. ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJMSlJRMWJqdTU1cDZhNDdSd2FkSCIsImlhdCI6MTY0OTIyOTQyMiwic3ViIjoidXNlci0xMjMiLCJ1c2VyIjp7ImVtYWlsIjoiam9lLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2UgRG9lIn0sImF1dGgiOnsiY29sbGFib3JhdGlvbiI6eyIqIjp7InJvbGUiOiJ3cml0ZXIifX19fQ._V-HXKKHU1E-saZxk4JTvgXdh1I7793nCEK91ubSZHY ``` ### Debugging For debugging purposes [jwt.io](https://jwt.io) can be used. source file: "cs/latest/examples/token-endpoints/nodejs.html" ## Token endpoint in Node.js This article presents a simple [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) example for creating JSON Web Tokens (JWT) implemented in Node.js. The tokens are used by CKEditor Cloud Services to authenticate users. ### Dependencies All examples use the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) library for creating tokens and [express](https://www.npmjs.com/package/express) to create the HTTP endpoint. ``` npm install express jsonwebtoken ``` ### Examples When creating a token endpoint to integrate with [Collaboration](#cs/latest/guides/collaboration/quick-start.html), the token payload should contain the environment ID and user data. #### Real-time collaboration features ``` const express = require( 'express' ); const jwt = require( 'jsonwebtoken' ); const accessKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy'; const environmentId = 'LJRQ1bju55p6a47RwadH'; const app = express(); app.use( ( req, res, next ) => { res.setHeader( 'Access-Control-Allow-Origin', '*' ); res.setHeader( 'Access-Control-Allow-Methods', 'GET' ); next(); } ); app.get( '/', ( req, res ) => { const payload = { aud: environmentId, sub: 'user-123', user: { email: 'joe.doe@example.com', name: 'Joe Doe' }, auth: { 'collaboration': { '*': { 'role': 'writer' } } } }; const result = jwt.sign( payload, accessKey, { algorithm: 'HS256', expiresIn: '24h' } ); res.send( result ); } ); app.listen( 8080, () => console.log( 'Listening on port 8080' ) ); ``` `accessKey` and `environmentId` should be replaced with the keys provided by the [Customer Portal](https://portal.ckeditor.com/) for SaaS or by the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. User data can be taken from the session or the database. You do not need to add `iat` because `jwt.sign()` will add it by itself. You should then pass the token to the client, for example by sending a plain string or by rendering a page that will contain this token. If the user is unauthenticated, the token endpoint should return an error or redirect to the login page. Also, you should make sure that the token is sent via an encrypted channel. #### CKEditor AI When creating a token endpoint for [CKEditor AI](#cs/latest/guides/ckeditor-ai/overview.html), the token payload should include AI-specific permissions in the `auth.ai.permissions` array. For a full list of available permissions, see the [Permissions guide](#cs/latest/guides/ckeditor-ai/permissions.html). ``` const express = require( 'express' ); const jwt = require( 'jsonwebtoken' ); const accessKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy'; const environmentId = 'LJRQ1bju55p6a47RwadH'; const app = express(); app.use( ( req, res, next ) => { res.setHeader( 'Access-Control-Allow-Origin', '*' ); res.setHeader( 'Access-Control-Allow-Methods', 'GET' ); next(); } ); app.get( '/', ( req, res ) => { const payload = { aud: environmentId, sub: 'user-123', user: { email: 'joe.doe@example.com', name: 'Joe Doe' }, auth: { 'ai': { 'permissions': [ 'ai:conversations:*', 'ai:models:agent', 'ai:actions:system:*', 'ai:reviews:system:*' ] } } }; const result = jwt.sign( payload, accessKey, { algorithm: 'HS256', expiresIn: '24h' } ); res.send( result ); } ); app.listen( 8080, () => console.log( 'Listening on port 8080' ) ); ``` If you use both collaboration and AI, combine both `auth.collaboration` and `auth.ai` in a single token payload. #### Easy Image, Export to PDF and Import and Export to Word The token endpoint for Easy Image and the Export to Word/PDF features does not require adding user data. You can therefore skip the `user` and `auth` properties in the token payload. #### Export to PDF and Import and Export to Word On-Premises Tokens for PDF Converter and DOCX Converter On-Premises do not require any additional claims, so you can create the token with an empty payload. In this implementation, `accessKey` has been replaced by `SECRET_KEY` \- a variable set during the [Import and Export to Word](#cs/latest/onpremises/docx-onpremises/installation.html)/[Export to PDF](#cs/latest/onpremises/pdf-onpremises/installation.html) On-Premises instance installation. ``` const express = require( 'express' ); const jwt = require( 'jsonwebtoken' ); const SECRET_KEY = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy'; const app = express(); app.use( ( req, res, next ) => { res.setHeader( 'Access-Control-Allow-Origin', '*' ); res.setHeader( 'Access-Control-Allow-Methods', 'GET' ); next(); } ); app.get( '/', ( req, res ) => { const result = jwt.sign( {}, SECRET_KEY, { algorithm: 'HS256' } ); res.send( result ); } ); app.listen( 8080, () => console.log( 'Listening on port 8080' ) ); ``` If you create your own token endpoint, do not forget to authenticate the user before you send the token. You can use [Passport](http://www.passportjs.org) for this. ### Usage Start the server by running: ``` node server ``` Now you can get the token with a simple request: ``` http://localhost:8080/ ``` ### Example response The result should be in a plain text format. ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJMSlJRMWJqdTU1cDZhNDdSd2FkSCIsImlhdCI6MTY0OTIyOTQyMiwic3ViIjoidXNlci0xMjMiLCJ1c2VyIjp7ImVtYWlsIjoiam9lLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2UgRG9lIn0sImF1dGgiOnsiY29sbGFib3JhdGlvbiI6eyIqIjp7InJvbGUiOiJ3cml0ZXIifX19fQ._V-HXKKHU1E-saZxk4JTvgXdh1I7793nCEK91ubSZHY ``` ### Debugging For debugging purposes, [jwt.io](https://jwt.io) can be used. source file: "cs/latest/examples/token-endpoints/php.html" ## Token endpoint in PHP This article presents a simple [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) example for creating JSON Web Tokens (JWT) implemented in PHP. The tokens are used by CKEditor Cloud Services to authenticate users. ### Dependencies All examples use the [firebase/php-jwt](https://firebaseopensource.com/projects/firebase/php-jwt/) library for creating tokens. ``` composer require firebase/php-jwt ``` ### Examples When creating a token endpoint to integrate with [Collaboration](#cs/latest/guides/collaboration/quick-start.html), the token payload should contain the environment ID and user data. #### Real-time collaboration features ``` $environmentId, 'iat' => time(), 'sub' => 'user-123', 'user' => array( 'email' => 'joe.doe@example.com', 'name' => 'Joe Doe' ), 'auth' => array( 'collaboration' => array( '*' => array( 'role' => 'writer', ) ) ) ); $jwt = JWT::encode($payload, $accessKey, 'HS256'); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. echo $jwt; ?> ``` `$accessKey` and `$environmentId` should be replaced with the keys provided by the [Customer Portal](https://portal.ckeditor.com/) for SaaS or by the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. User data can be taken from the session or the database. You should then pass the token to the client, for example by sending a plain string or by rendering a page that will contain this token. If the user is unauthenticated, the token endpoint should return an error or redirect to the login page. Also, you should make sure that the token is sent via an encrypted channel. #### CKEditor AI When creating a token endpoint for [CKEditor AI](#cs/latest/guides/ckeditor-ai/overview.html), the token payload should include AI-specific permissions in the `auth.ai.permissions` array. For a full list of available permissions, see the [Permissions guide](#cs/latest/guides/ckeditor-ai/permissions.html). ``` $environmentId, 'iat' => time(), 'sub' => 'user-123', 'user' => array( 'email' => 'joe.doe@example.com', 'name' => 'Joe Doe' ), 'auth' => array( 'ai' => array( 'permissions' => array( 'ai:conversations:*', 'ai:models:agent', 'ai:actions:system:*', 'ai:reviews:system:*' ) ) ) ); $jwt = JWT::encode($payload, $accessKey, 'HS256'); echo $jwt; ?> ``` If you use both collaboration and AI, combine both `auth.collaboration` and `auth.ai` in a single token payload. #### Easy Image, Export to PDF and Import and Export to Word The token endpoint for Easy Image and the Export to Word/PDF features does not require adding user data. You can therefore skip the `user` and `auth` properties in the token payload. #### Export to PDF and Import and Export to Word On-Premises Tokens for PDF Converter and DOCX Converter On-Premises do not require any additional claims, so you can create the token with an empty payload. In this implementation, `accessKey` has been replaced by `SECRET_KEY` \- a variable set during the [Import and Export to Word](#cs/latest/onpremises/docx-onpremises/installation.html)/[Export to PDF](#cs/latest/onpremises/pdf-onpremises/installation.html) On-Premises instance installation. ``` time() ); $jwt = JWT::encode($payload, $secretKey, 'HS256'); // Here we are printing the token to the console. In a real usage scenario // it should be returned in an HTTP response of the token endpoint. echo $jwt; ?> ``` If you create your own token endpoint, do not forget to authenticate the user before you send the token. ### Example response The result should be in a plain text format. ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJMSlJRMWJqdTU1cDZhNDdSd2FkSCIsImlhdCI6MTY0OTIyOTQyMiwic3ViIjoidXNlci0xMjMiLCJ1c2VyIjp7ImVtYWlsIjoiam9lLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2UgRG9lIn0sImF1dGgiOnsiY29sbGFib3JhdGlvbiI6eyIqIjp7InJvbGUiOiJ3cml0ZXIifX19fQ._V-HXKKHU1E-saZxk4JTvgXdh1I7793nCEK91ubSZHY ``` ### Debugging For debugging purposes [jwt.io](https://jwt.io) can be used. source file: "cs/latest/examples/token-endpoints/python.html" ## Token endpoint in Python 3 This article presents a simple [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) example for creating JSON Web Tokens (JWT) implemented in python. The tokens are used by CKEditor Cloud Services to authenticate users. ### Dependencies All code examples use the [pyjwt](https://pypi.org/project/pyjwt/), [Flask](https://pypi.org/project/Flask/) and [Flask-Cors](https://pypi.org/project/Flask-Cors/) libraries. If you are using [Python Package Index](https://pypi.org/), you can run the following command in a terminal: ``` python3 -m pip install flask flask_cors pyjwt ``` ### Examples When creating a token endpoint to integrate with [Collaboration](#cs/latest/guides/collaboration/quick-start.html), the token payload should contain the environment ID and user data. #### Real-time collaboration features ``` import jwt from time import time from flask import Flask from flask_cors import CORS accessKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy' environmentId = 'LJRQ1bju55p6a47RwadH' app = Flask(__name__) CORS(app) @app.route('/') def main(): timestamp = int(time()) payload = { 'aud': environmentId, 'iat': timestamp, 'sub': 'user-123', 'user': { 'email': 'joe.doe@example.com', 'name': 'Joe Doe' }, 'auth': { 'collaboration': { '*': { 'role': 'writer' } } } } return jwt.encode(payload, accessKey) if __name__ == '__main__': app.run(port='8080') ``` `accessKey` and `environmentId` should be replaced with the keys provided by the [Customer Portal](https://portal.ckeditor.com/) for SaaS or by the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. User data can be taken from the session or the database. You should then pass the token to the client, for example by sending a plain string or by rendering a page that will contain this token. If the user is unauthenticated, the token endpoint should return an error or redirect to the login page. Also, you should make sure that the token is sent via an encrypted channel. #### CKEditor AI When creating a token endpoint for [CKEditor AI](#cs/latest/guides/ckeditor-ai/overview.html), the token payload should include AI-specific permissions in the `auth.ai.permissions` array. For a full list of available permissions, see the [Permissions guide](#cs/latest/guides/ckeditor-ai/permissions.html). ``` import jwt from time import time from flask import Flask from flask_cors import CORS accessKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy' environmentId = 'LJRQ1bju55p6a47RwadH' app = Flask(__name__) CORS(app) @app.route('/') def main(): timestamp = int(time()) payload = { 'aud': environmentId, 'iat': timestamp, 'sub': 'user-123', 'user': { 'email': 'joe.doe@example.com', 'name': 'Joe Doe' }, 'auth': { 'ai': { 'permissions': [ 'ai:conversations:*', 'ai:models:agent', 'ai:actions:system:*', 'ai:reviews:system:*' ] } } } return jwt.encode(payload, accessKey) if __name__ == '__main__': app.run(port='8080') ``` If you use both collaboration and AI, combine both `auth.collaboration` and `auth.ai` in a single token payload. #### Easy Image, Export to PDF and Import and Export to Word The token endpoint for Easy Image and the Export to Word/PDF features does not require adding user data. You can therefore skip the `user` and `auth` properties in the token payload. #### Export to PDF and Import and Export to Word On-Premises Tokens for PDF Converter and DOCX Converter On-Premises do not require any additional claims, so you can create the token with an empty payload. In this implementation, `accessKey` has been replaced by `SECRET_KEY` \- a variable set during the [Import and Export to Word](#cs/latest/onpremises/docx-onpremises/installation.html)/[Export to PDF](#cs/latest/onpremises/pdf-onpremises/installation.html) On-Premises instance installation. ``` import jwt from time import time from flask import Flask from flask_cors import CORS secretKey = 'w1lnWEN63FPKxBNmxHN7WpfW2IoYVYca5moqIUKfWesL1Ykwv34iR5xwfWLy' app = Flask(__name__) CORS(app) @app.route('/') def main(): timestamp = int(time()) payload = { 'iat': timestamp } return jwt.encode( payload, secretKey) if __name__ == '__main__': app.run(port='8080') ``` If you create your own token endpoint, do not forget to authenticate the user before you send the token. ### Usage Start the server by running: ``` python index.py ``` Now you can get the token with a simple request: ``` http://localhost:8080/ ``` ### Example response The response should be in a plain text format. ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJMSlJRMWJqdTU1cDZhNDdSd2FkSCIsImlhdCI6MTY0ODgwNzY2Miwic3ViIjoidXNlci0xMjMiLCJ1c2VyIjp7ImVtYWlsIjoiam9lLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2UgRG9lIn0sImF1dGgiOnsiY29sbGFib3JhdGlvbiI6eyIqIjp7InJvbGUiOiJ3cml0ZXIifX19fQ.QZLzRz9SF8JP2zK9vENewmD75Og6z1a83fDt5SXuLF4 ``` ### Debugging For debugging purposes [jwt.io](https://jwt.io) can be used. source file: "cs/latest/examples/webhooks/webhooks-server-nodejs.html" ## Webhooks server in Node.js This article presents a simple [webhooks server](#cs/latest/developer-resources/webhooks/server.html) example. ### Dependencies Both examples use the Express library to create the HTTP endpoint. Also, for local development purposes, a tunneling service is required. This example uses [ngrok](https://ngrok.com/). ``` npm install express jsonwebtoken ``` You can download ngrok here: . ### Examples Below are two examples of a webhooks server built on Express. #### Example without checking the request signature This is a very simple example, with the server logging the `body` from the request to the console. The `body` contains the complete [webhook information](#cs/latest/developer-resources/webhooks/overview.html--webhook-format) sent from CKEditor Cloud Services. ``` const express = require( 'express' ); const app = express(); app.use( express.json() ); app.post( '/', ( req, res ) => { console.log( 'received webhook', req.body ); res.sendStatus( 200 ); } ); app.listen( 9000, () => console.log( 'Node.js server started on port 9000.' ) ); ``` #### Example with checking the request signature This example is more complex because while the server logs the webhook information to the console, it also checks if the request was sent from the CKEditor Cloud Services servers and was [signed](#cs/latest/developer-resources/security/request-signature.html) with the correct [API secret](#cs/latest/developer-resources/security/api-secret.html). Several variables are needed to generate and check the signature. The [API secret](#cs/latest/developer-resources/security/api-secret.html) is available in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises, the rest of the parameters are in the request: * `method`: `req.method` * `url`: `req.url` * `timestamp`: `req.headers[ 'x-cs-timestamp' ]` * `body`: `req.rawBody` Please note that the `rawBody` field was added by the following configuration: ``` app.use( express.json( { verify: ( req, res, buffer ) => { req.rawBody = buffer; } } ) ); ``` This field is not available by default in Express. The `body` field available in Express contains the already processed data that cannot be used to generate the signature. ``` const crypto = require( 'crypto' ); const express = require( 'express' ); const app = express(); const API_SECRET = 'secret'; app.use( express.json( { verify: ( req, res, buffer ) => { req.rawBody = buffer; } } ) ); app.post( '/', ( req, res ) => { const signature = _generateSignature( req.method, req.url, req.headers[ 'x-cs-timestamp' ], req.rawBody ); if ( signature !== req.headers[ 'x-cs-signature' ] ) { return res.sendStatus( 401 ); } console.log( 'received webhook', req.body ); res.sendStatus( 200 ); } ); app.listen( 9000, () => console.log( 'Node.js server started on port 9000.' ) ); function _generateSignature( method, url, timestamp, body ) { const hmac = crypto.createHmac( 'SHA256', API_SECRET ); hmac.update( `${ method.toUpperCase() }${ url }${ timestamp }` ); if ( body ) { hmac.update( body ); } return hmac.digest( 'hex' ); } ``` ### Usage Start the server with: ``` node index.js ``` If the server is running on port `9000`, run ngrok with: ``` ./ngrok http 9000 ``` After this, you should see a `*.ngrok.io` URL. Copy the `*.ngrok.io` URL and paste it in the webhook configuration. You should be able to receive webhooks now. source file: "cs/latest/examples/webhooks/webhooks-server-php.html" ## Webhooks server in PHP This article presents a simple [webhooks server](#cs/latest/developer-resources/webhooks/server.html) example. ### Dependencies This example does not require any external PHP dependencies. However, for local development purposes, a tunneling service is required. This example uses [ngrok](https://ngrok.com/). You can download ngrok here: . ### Examples Below are two examples of a webhooks server. #### Example without checking the request signature This is a very simple example, with the server logging the `body` from the request to the console. The `body` contains the complete [webhook information](#cs/latest/developer-resources/webhooks/overview.html--webhook-format) sent from CKEditor Cloud Services. ``` #### Example with checking the request signature This example shows how to verify if the request was sent from the CKEditor Cloud Services servers and was [signed](#cs/latest/developer-resources/security/request-signature.html) with the correct [API secret](#cs/latest/developer-resources/security/api-secret.html). Several variables are needed to generate and check the signature. The [API secret](#cs/latest/developer-resources/security/api-secret.html) is available in the [Customer Portal](https://portal.ckeditor.com/) for SaaS or in the Management Panel for On-Premises, the rest of the parameters are in the request: * `method`: `$_SERVER['REQUEST_METHOD']` * `url`: `$_SERVER['REQUEST_URI']` * `timestamp`: `$_SERVER['HTTP_X_CS_TIMESTAMP']` * `body`: `file_get_contents('php://input')` ``` ### Usage Save the above example file as `index.php` and start the server with: ``` php -S 127.0.0.1:9000 ``` If needed, run ngrok with: ``` ./ngrok http 9000 ``` After this, you should see a `*.ngrok.io` URL. Copy the `*.ngrok.io` URL and paste it in the webhook configuration. You should be able to receive webhooks now. source file: "cs/latest/faq/index.html" ## Cloud Services FAQ ### How to update the editor bundle? An **editor bundle** is the source code of a pre-configured and ready-to-use CKEditor 5, which is minified and built as a single file. To use the document storage, import and export or connection optimization features, you need to upload an exact copy of the editor file from your application to the CKEditor Cloud Services server. Read more about an editor bundle [here](#cs/latest/guides/collaboration/editor-bundle.html). To ensure data consistency, it is recommended to update the editor bundle every time there is a change in your editor. For instructions on how to update the editor bundle, check out the [documentation](#cs/latest/guides/collaboration/editor-bundle.html--updating-the-editor-bundle). ### How can I update the editor? After updating CKEditor 5 in your frontend application you may receive the `Incompatible bundle version or engine version detected` error while connecting to a document. It means the document was created using an older version of the editor. In such a case, you need to create a new collaboration session to keep working with the document. There is no way to edit a document using two or more different versions of the editor. To create a new collaboration session, you can disconnect all users and reconnect after 24 hours. Alternatively, you can end the open collaboration session programmatically using our [REST API method](https://docs.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete). ### How to detect an incompatible bundle version? To check the bundle version which was used to initialize an editing session, you can perform a request to our [REST API](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D~1details/get). In the response you will receive `bundle_version` which can be compared with the current [bundle version](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle). If the bundle versions are different, users will not be able to connect to the document using the new editor. In that case, you can [flush the collaboration session](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete), but keep in mind that flushing it will break the document for the users that are already connected with an old editor. You can check whether anyone is still connected using a [dedicated REST API method](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D~1users/get). ### How to restore a corrupted document? Sometimes a document can get broken as a result of some invalid operations. They can be caused by custom plugins, using an invalid bundle version, etc. In such a case, you will receive an error saying `The document cannot be accessed in the current state. Some operations could not be saved properly`. The only way to recover such a document is by restoring it to the latest working version via the [REST API endpoint](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D~1restore/put). This method will remove the operations that break the document and prevent it from saving. ### How to save documents? CKEditor Cloud Services provide 3 ways of saving documents. By default, our services do not store customer documents permanently. To do so, you need to enable [the document storage feature](#cs/latest/guides/collaboration/document-storage.html). You can also store documents in your own storage by using our [REST API methods](#cs/latest/guides/collaboration/import-and-export.html). The third option is to save a document directly from your frontend application with the help of [the autosave plugin](#cs/latest/guides/collaboration/introduction.html--the-autosave-plugin). For more details, including a comparison of the available save methods, check out [our documentation](#cs/latest/guides/collaboration/introduction.html--saving-the-collaboration-data). ### How to export the content of a document? As soon as the document is created, you can make a request to our REST API to fetch its content. As a result, you will receive an HTML with custom tags containing the data added by the plugins. You can find more information about exporting document content in a [dedicated section](#cs/latest/guides/collaboration/import-and-export.html). ### How to create a document programmatically? By default, a document is created when the first user connects to it. However, you can import a document before any user connects to it by using some previously exported document content. For more information about importing document content, check out [our documentation](#cs/latest/guides/collaboration/import-and-export.html). ### How to get comments? The content of the comments and their metadata are kept in a database. The document content contains tags with comment IDs referencing the data in the database. You can fetch the content of the chosen comment by performing a request to our [REST API](https://help.cke-cs.com/api/v5/docs#tag/Comments/paths/~1comments~1%7Bcomment%5Fid%7D/get). It is also possible to [get all comments in the given document](https://help.cke-cs.com/api/v5/docs#tag/Comments/paths/~1comments/get). ### How to get suggestions? Suggestions are embedded in a given document’s content, within dedicated tags. The database contains only the metadata, such as a suggestion’s state, its author, etc. You can fetch suggestion metadata by performing a request to our [REST API](https://help.cke-cs.com/api/v5/docs#tag/Suggestions/paths/~1suggestions~1%7Bsuggestion%5Fid%7D/get). ### How to remove all the data from a given environment? CKEditor Cloud Services [keeps some of the data in a persistent database](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data). However, all of this data is wiped out when removing a given environment. Instructions on how to remove an environment can be found in a [dedicated section](#cs/latest/developer-resources/environments/environments-management.html--removing-an-environment). All billing data stays untouched after such an operation, the monitoring data is removed automatically after 30 days. ### How to resend a webhook? In some cases, e.g. given an error in the webhook destination server, you may need to resend the webhook request manually. To perform such an operation, visit the `Webhooks` section in the [Customer Portal](https://portal.ckeditor.com) for SaaS or in the [Management Panel](#cs/latest/onpremises/cs-onpremises/management.html) for the On-Premises application. From the list of created webhooks, select the one you are interested in. Next, on the list of requests, find the one you would like to resend. After selecting the request, click the `Resend the request` button, then confirm the operation in the following pop-up. ### Are identifiers such as `userId`, `documentId`, etc. case-sensitive? All identifiers such as `userId`, `documentId`, etc. are case-sensitive. It means that e.g. `user1` and `User1` will be treated as two separate users. The only exception are CKEditor Collaboration Server On-Premises installations up to `4.12.1` for MySQL which are case-insensitive. #### Migrating from case-insensitive to case-sensitive CKEditor Collaboration Server On-Premises installation If you would like to migrate from a case-insensitive installation, you can set up a fresh CKEditor Collaboration Server On-Premises installation on a version newer than `4.12.1` and use the [Export](https://help.cke-cs.com/api/v5/docs#tag/Documents/paths/~1documents~1%7Bsource%5Fdocument%5Fid%7D/post) and [Import](https://help.cke-cs.com/api/v5/docs#tag/Documents/paths/~1documents/post) REST API endpoints to migrate your content. A simple update of an old CKEditor Collaboration Server On-Premises above `4.12.1` does not convert the instance to be case-sensitive. Instead, a fresh installation with the migration process is required. source file: "cs/latest/guides/basic-concepts/authorization.html" ## Authorization In this article, you will find authorization-related basic concepts used in the documentation. CKEditor Cloud Services have a few authorization methods, and each of them is suited for a given communication way. To find more detailed information, please check the linked articles. The diagram below shows what authorization methods are available and where given operations need to be performed. ### API Secret An API secret is a secret used to generate a [signature](#cs/latest/developer-resources/security/request-signature.html) for requests made to the REST API. It is also used to [verify webhooks requests](#cs/latest/developer-resources/webhooks/overview.html--security). API secrets can be [managed through the Customer Portal, or through the Management Panel in case of On-premises installations](#cs/latest/developer-resources/security/api-secret.html--api-secrets-management). ### Access Key An Access Key is a key used to [generate signed tokens](#cs/latest/developer-resources/security/token-endpoint.html). Access Keys are generated in [the Customer Portal or in the Management Panel in case of On-premises installations](#cs/latest/developer-resources/security/access-key.html--managing-the-access-credentials). ### Token endpoint An endpoint created by a customer which returns a token. It needs to be created by customers and placed in the customers’ application. Thanks to that, a customer has the full ability to control the user data and privileges sent. You can read more about the token endpoint in the [Token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) guide. ### Token It is returned from a Token endpoint. In the payload, it contains all user data and [roles and permissions](#cs/latest/developer-resources/security/roles.html) of the user to a given document. Based on a `sub` property of the token, the usage of the application is billed. You can read more about tokens and their generation in a [dedicated section](#cs/latest/developer-resources/security/token-endpoint.html--token). source file: "cs/latest/guides/basic-concepts/data-model.html" ## Data models In this article, you will get familiar with basic data structures mentioned in other articles. To find more detailed information, please check one of the linked articles. The diagram below, shows how the mentioned data structures are organized and how are they related. ### Organization Organization is the widest entity in a context of Cloud Services provided by CKSource. Organization groups environments created by a customer. Depending on a case, single customer can have multiple, totally independent, organizations. For CKEditor Collaboration Server On-premises customers there is only one, default organization in a context of a single application installation, what is fully transparent while using the application. ### Environment A single organization consist of multiple environments, which ensures logical data separation. Data of each environment are still held in a single database, however there are encrypted using individual keys. It ensures that there are not any chances to fetch others environment’s data by accident. Webhooks, editor bundles, document storage and other features needs to be configured separately for each environment. One of use-cases of having multiple environments are multiple stages of the application, e.g. “Development”, “Staging”, “QA” and “Production”. For an On-premises installation it allows to create multitenant applications, where each end-customer utilizes separate set of data with separated secrets, keys and encryption. Environments can be [managed through the Customer Portal, or through the Management Panel in case of On-premises installations](#cs/latest/guides/basic-concepts/management.html). ### Document Document is a set of data representing the most basic entity in a context of the CKEditor Collaboration Server. It is logical structure, which describes linked pieces of information. In the application it is identified by `documentId` or `channelId`. Each document can have comments, suggestions, revisions, etc., assigned to it. Documents can be also managed through the [Documents API](https://help.cke-cs.com/api/docs#tag/Documents) in the [REST API](#cs/latest/developer-resources/apis/overview.html--ckeditor-cloud-services-restful-apis). ### Editing/Collaboration session While the real-time collaboration plugin connects to a specific document on a CKEditor Collaboration Server, a new editing session is created. The editing session in an instance of a document, which is held in a temporary memory, where all content changes from the editor are applied. Each user who connects to a single document, connects to the same editing session where all changes of content are propagated to all connected users. An editing session can be initialized in a multiple ways, which are explained in [a dedicated section](#cs/latest/guides/collaboration/introduction.html--ways-of-initializing-the-collaboration-session-with-data). 24 hours after the last user disconnects, the editing session expires and it is removed from a temporary memory. The document content can be saved using various methods, depending on a use-case. You can read more about saving documents [in a more detailed guide](#cs/latest/guides/collaboration/introduction.html--saving-the-collaboration-data). Editing sessions can be managed using the [Collaboration API](https://help.cke-cs.com/api/v5/docs#tag/Collaboration) in the [REST API](#cs/latest/developer-resources/apis/overview.html--ckeditor-cloud-services-restful-apis). source file: "cs/latest/guides/basic-concepts/editor-bundle.html" ## Editor bundle An editor bundle is the source code of a pre-configured and ready-to-use CKEditor 5, which is minified and built as a single file. To use the [Server-side Editor API](#cs/latest/guides/collaboration/server-side-editor-api.html), [document storage](#cs/latest/guides/collaboration/document-storage.html), [import and export](#cs/latest/guides/collaboration/import-and-export.html), or [connection optimization](#cs/latest/guides/collaboration/connection-optimization.html) features, you need to upload an exact copy of the editor file from your application to the CKEditor Cloud Services server. CKEditor Cloud Services uses your editor bundle to convert operations in the collaboration session into data. Thanks to this, CKEditor Cloud Services can ensure data consistency between users and the stored data. Furthermore, if your editor bundle is using some custom plugins that can output some custom data, it will be then properly handled by CKEditor Cloud Services. Whenever you change something in your editor bundle or its configuration, you should update it on the CKEditor Cloud Services server, too. If the users use different versions of the editor than the one uploaded to the server, it may lead to data loss or even editor crashes. More information and detailed instructions about using editor bundles can be found in a [dedicated section](#cs/latest/guides/collaboration/editor-bundle.html--building-the-editor-bundle). source file: "cs/latest/guides/basic-concepts/management.html" ## Management CKEditor Cloud Services provides tools to manage [environments](#cs/latest/guides/basic-concepts/data-model.html--environment) and other features. These tools are provided in the form of a web application. ### Customer Portal A place where customers can manage their existing license subscriptions and products. On-Premises subscribers will find credentials needed to pull Docker images there as well as license keys for their applications. For SaaS users, it also provides a panel for managing [environments](#cs/latest/guides/basic-concepts/data-model.html--environment), [access keys](#cs/latest/guides/basic-concepts/authorization.html--access-key), [API secrets](#cs/latest/guides/basic-concepts/authorization.html--api-secret), etc. On-premises users can access this panel via the [Management Panel](#cs/latest/guides/basic-concepts/management.html--management-panel) for On-Premises. The Customer Portal is available at [portal.ckeditor.com](https://portal.ckeditor.com/). ### Management Panel This is where On-Premises users can manage their [environments](#cs/latest/guides/basic-concepts/data-model.html--environment), [access keys](#cs/latest/guides/basic-concepts/authorization.html--access-key), [API secrets](#cs/latest/guides/basic-concepts/authorization.html--api-secret), etc. It can be found under the `/panel` path on the server where CKEditor Collaboration Server On-premises is running. More information about the Management Panel in the On-premises version can be found in a [dedicated guide](#cs/latest/onpremises/cs-onpremises/management.html). source file: "cs/latest/guides/ckeditor-ai/actions.html" ## Actions Transform specific text content with focused operations such as grammar fixing and translation. Actions are single-purpose tools that take content in, apply a transformation, and return the result. Unlike conversations, actions do not remember previous interactions. Each action is independent and focused on a single task. **When to use Actions vs Reviews:** Use Actions when you need to transform specific text content (fix grammar, translate, adjust tone). Use Reviews when you need to analyze entire documents for quality improvements and get suggestions without automatically changing the content. ### Available Actions #### System Actions CKEditor AI provides built-in system actions for common content transformation tasks: * **Fix Grammar** – Catch and correct spelling mistakes, grammar errors, and punctuation issues. * **Improve Writing** – Enhance clarity, flow, and overall writing quality. * **Continue Writing** – Complete unfinished sentences, paragraphs, or entire documents. * **Make Longer** – Expand content with more detail, examples, and explanations. * **Make Shorter** – Condense lengthy text while keeping the essential information. * **Adjust Tone** – Change writing style to casual, professional, friendly, or confident. * **Translate** – Convert content between languages with proper cultural context. #### Custom Actions In addition to system actions, you can create custom actions tailored to your specific use cases. Custom actions allow you to define specialized content transformations using your own prompts to control AI behavior. Unlike system actions that use predefined identifiers, custom actions use a unified endpoint where you define the transformation behavior through a prompt parameter. ### Key Features Each action is independent and does not require conversation context. Actions use streaming output with Server-Sent Events for real-time feedback as results are generated. ### API Examples #### Grammar Fix Example ``` POST /v1/actions/system/fix-grammar/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

The norhtern lights dence across the polar skies, painting ribbons of green and purple light that ripple like a cosmic curtain.

" } ] } ```
#### Writing Improvement Example ``` POST /v1/actions/system/improve-writing/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

The system works by processing data through various algorithms to produce results.

" } ] } ```
#### Content Expansion ``` POST /v1/actions/system/make-longer/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

Artificial intelligence is transforming the way we work.

" } ] } ```
#### Content Condensation Example ``` POST /v1/actions/system/make-shorter/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

Artificial intelligence, which is a rapidly evolving field of computer science that focuses on creating intelligent machines capable of performing tasks that typically require human intelligence, is transforming the way we work across various industries and sectors.

" } ] } ```
#### Tone Adjustment Example ``` POST /v1/actions/system/make-tone-casual/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

We regret to inform you that your request cannot be processed at this time.

" } ] } ```
#### Translation Example ``` POST /v1/actions/system/translate/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

Hello, how are you today?

" } ], "args": { "language": "Spanish" } } ```
#### Custom Action Example ``` POST /v1/actions/custom/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

The company's Q4 revenue was $2.5M, representing a 15% increase YoY.

" } ], "prompt": "Convert financial abbreviations to full words (e.g., 'YoY' to 'year-over-year', 'Q4' to 'fourth quarter') to make the text more accessible to general audiences.", "model": "agent-1", "outputFormat": "html" } ``` Custom actions require the `ai:actions:custom` permission in your JWT token.
### Streaming Responses Actions use Server-Sent Events (SSE) for real-time streaming results. See the [Streaming Responses guide](#cs/latest/guides/ckeditor-ai/streaming.html) for detailed implementation information. ### API Reference For complete endpoint documentation, request/response schemas, authentication details, and additional parameters, see: * **[Actions API Reference](https://ai.cke-cs.com/docs#tag/Actions)** – Full documentation for system and custom actions endpoints. * **[Complete API Documentation](https://ai.cke-cs.com/docs)** – Interactive API reference with all CKEditor AI endpoints. ### Related Features * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – For interactive discussions with document analysis and context. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – For content quality analysis and improvement suggestions. * [Streaming Responses](#cs/latest/guides/ckeditor-ai/streaming.html) – For implementing real-time review suggestions. source file: "cs/latest/guides/ckeditor-ai/conversations.html" ## Conversations Conversations allow you to exchange multiple messages with AI that maintains them in its context. Conversations can be extended by external context sources like websites or files, and have the ability to refer to editor content and suggest modifications. ### Key Features Upload PDFs, Word docs, and images for the AI to read and understand. Ask questions about specific sections and get intelligent answers. The AI extracts text while preserving structure from PDFs, maintains formatting context from Word documents, parses web content from HTML files, and processes images with OCR and object recognition. Each conversation builds on previous messages, so the AI keeps track of your entire discussion and any files you have shared. You can mix documents, images, web links, and text in one conversation, and the AI connects information across all formats. Enable web search for real-time research while keeping your conversation context. #### Example: Product Launch Workflow 1. **Upload product spec** → _“What are the key features for marketing?”_ 2. **Add competitor analysis** → _“How do we compare to competitors?”_ 3. **Reference blog post** → _“Write a press release using this blog post and our competitive advantages”_ 4. **Include brand guidelines** → _“Match our brand voice and key messaging”_ The AI remembers everything you have shared and builds on it throughout your conversation. ### Advanced Features #### Web Search Enable real-time web search to access current information during conversations. The AI searches the web for relevant content, processes and analyzes the results, and integrates findings into responses while maintaining conversation context. Configure via the `webSearch` capability in API requests. #### Reasoning Enable step-by-step reasoning to see the AI’s problem-solving process. The AI breaks down complex queries into logical steps, considers multiple approaches, and can revise conclusions when new information emerges. Configure via the `reasoning` capability in API requests. ### API Reference For complete API documentation including endpoints, parameters, and response schemas, see the [REST API documentation](https://ai.cke-cs.com/docs#tag/Conversations). ### API Examples #### Create a Conversation ``` POST /v1/conversations Content-Type: application/json Authorization: Bearer { "id": "my-conversation-123", "title": "Document Analysis Session", "group": "research" } ``` #### Upload a Document Before you can reference documents in conversations, you need to upload them first: ``` POST /v1/conversations/my-conversation-123/documents Content-Type: multipart/form-data Authorization: Bearer file: [your-document.pdf] ``` Response: ``` { "id": "doc-123", } ``` You can see how to upload other resources, including web resources and files using [REST API documentation](https://ai.cke-cs.com/docs#tag/Conversations). #### Send a Message with Context ``` POST /v1/conversations/my-conversation-123/messages Content-Type: application/json Authorization: Bearer { "prompt": "Analyze the attached document and provide a summary of the key points", "model": "agent-1", "content": [ { "type": "document", "id": "doc-123" } ], "capabilities": { "webSearch": {}, "reasoning": {} } } ``` #### Send a Message with Multiple Context Types ``` POST /v1/conversations/my-conversation-123/messages Content-Type: application/json Authorization: Bearer { "prompt": "Compare the attached document with the information from the web resource", "model": "agent-1", "content": [ { "type": "document", "id": "doc-123" }, { "type": "web-resource", "id": "web-123" }, ], "capabilities": { "webSearch": {} } } ``` ### Streaming Responses Conversations use Server-Sent Events (SSE) for real-time streaming responses. See the [Streaming Responses guide](#cs/latest/guides/ckeditor-ai/streaming.html) for detailed implementation information and code examples. ### Related Features * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – Content quality analysis and improvement suggestions. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – Content transformation and batch processing. * [AI Models](#cs/latest/guides/ckeditor-ai/models.html) – Choosing the right AI model for your conversations. * [Streaming Responses](#cs/latest/guides/ckeditor-ai/streaming.html) – Implementing real-time conversation features. source file: "cs/latest/guides/ckeditor-ai/document-processing.html" ## Document Processing Transform entire HTML documents with free-form AI instructions — no editor instance, no streaming, no conversation state. Send a document and a natural-language prompt to a single REST endpoint. The API returns the modified document and a summary of what changed as synchronous JSON. Built for server-side integrations, content pipelines, and backend automation. ### How It Works The endpoint accepts two fields: * **`document`** – The HTML content to process. * **`prompt`** – A natural language instruction describing the desired change. The AI applies the instruction and returns: * **`document`** – The complete modified HTML document. * **`summary`** – A concise explanation of what was changed and why. The response preserves HTML structure, attributes and CSS classes unless the instruction explicitly requires modifying them. ### When to use this Document Processing is designed for server-side workflows where content flows through your systems without a user in an editor: * **CMS publish hooks** — fix grammar or adjust tone before content goes live * **Sequential processing** — apply the same instruction to contracts, policies, or templates one document at a time * **Pipeline steps** — translate or summarize as one step in a larger workflow * **SaaS personalization** — adapt templates before delivering to end users * **AI agent tooling** — expose as a tool in your own AI agents or MCP servers ### API Example ``` POST /v1/documents/process Content-Type: application/json Authorization: Bearer { "document": "

The norhtern lights dence across the polar skies, painting ribbons of green and purple light that ripple like a cosmic curtain.

", "prompt": "Fix all grammar and spelling errors" } ```
### API Reference For complete endpoint documentation, request/response schemas, authentication details, and error codes, see: * **[Document processing API Reference](https://ai.cke-cs.com/docs#tag/Document-processing)** – Full documentation for the document processing endpoint. * **[Complete API Documentation](https://ai.cke-cs.com/docs)** – Interactive API reference with all CKEditor AI endpoints. ### Related Features * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – Transform content with predefined or custom operations. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – Analyze documents and get per-element improvement suggestions. * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – Interactive discussions with document context. source file: "cs/latest/guides/ckeditor-ai/limits.html" ## Limits Understand the limits that ensure fair usage, optimal performance, and cost control across all CKEditor AI features. ### Overview CKEditor AI implements various limits to ensure fair usage, optimal performance, and cost control. These include rate limits for API requests, context limits for content size and processing, model-specific constraints, and file restrictions. ### Rate Limits Rate limits control the frequency of API requests to prevent abuse and ensure service stability. The service implements limits on API requests, token usage, web search, and web scraping requests per minute. All rate limits are applied at both organization level (higher limits) and individual user level (lower limits) to ensure fair usage. ### Context Limits Context limits control how much content can be attached to conversations to ensure AI models can process all information effectively. These limits vary by model based on their specific capabilities and processing requirements. #### File Limits Files are limited to 7MB each (PDF, DOCX, PNG, JPEG, Markdown, HTML, Plain text). You can upload up to 100 files per conversation with a total size limit of 30MB. PDF files are limited to 100 pages total across all PDFs in a conversation. ##### Model-specific file limits Anthropic and agent models may use files up to 5MB each. #### Context Optimization Tips Compress images and split large documents into smaller sections. Use text formats (TXT or MD) over PDF when possible for better processing. Attach only relevant files to conversations and provide document summaries for very large files. ### Model-Specific Limits Different AI models have varying capabilities and limitations that affect context processing. Each model has different context window sizes that determine how much content can be processed. Models have response timeouts, file processing timeouts, web resource timeouts, and streaming response limits. All models include content moderation for inappropriate content, safety checks, and moderation response time limits. ### Next Steps * [Learn about AI Models](#cs/latest/guides/ckeditor-ai/models.html) for model-specific limitations. * [Set up Permisssions](#cs/latest/guides/ckeditor-ai/permissions.html) to control user access. * [Explore Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) for context management. * [API Documentation](https://ai.cke-cs.com/docs) – Complete API reference for CKEditor AI. source file: "cs/latest/guides/ckeditor-ai/models.html" ## AI Models Choose the right AI model for your needs. CKEditor AI provides access to leading AI models, each optimized for different use cases and performance requirements. ### Recommended: Agent Models CKEditor AI provides agent models that automatically select the optimal AI model for your specific task, balancing speed, quality, and cost. Agent models are internally registered and versioned, with the exact model name varying by compatibility version, for example, `agent-1`. **Benefits:** * Automatic optimization for each request type. * No need to understand individual model capabilities. * Cost-effective routing to appropriate models. * Continuous improvement as new models become available. For most use cases, we recommend using the agent model appropriate for your compatibility version and letting the system handle model selection. ### Available Models | Model ID | Name | Provider | Web Search | Reasoning | | ----------------- | ----------------- | --------- | ---------- | --------- | | agent-1 | Agent | Agent | Yes | Yes | | gpt-5 | GPT-5 | OpenAI | Yes | Yes | | gpt-5.1 | GPT-5.1 | OpenAI | Yes | Yes | | gpt-5.2 | GPT-5.2 | OpenAI | Yes | Yes | | gpt-5.4 | GPT-5.4 | OpenAI | Yes | Yes | | gpt-5.4-mini | GPT-5.4 Mini | OpenAI | Yes | Yes | | gpt-4.1 | GPT-4.1 | OpenAI | Yes | No | | gpt-5-mini | GPT-5 Mini | OpenAI | Yes | Yes | | gpt-4.1-mini | GPT-4.1 Mini | OpenAI | Yes | No | | claude-4-5-sonnet | Claude 4.5 Sonnet | Anthropic | Yes | Yes | | claude-4-6-sonnet | Claude 4.6 Sonnet | Anthropic | Yes | Yes | | claude-4-5-haiku | Claude 4.5 Haiku | Anthropic | Yes | Yes | | gemini-3-1-pro | Gemini 3.1\. Pro | Google | Yes | Yes | | gemini-2-5-flash | Gemini 2.5 Flash | Google | Yes | Yes | | gemini-3-flash | Gemini 3 Flash | Google | Yes | Yes | All models support conversations, reviews, and actions. Each model has specific limits for prompt length, conversation length, file handling, and other constraints. For current specifications and availability in your environment, query the `/v1/models` endpoint. ### How Model Selection Works The platform supports models from major AI providers including OpenAI, Google, and Anthropic. Each model has different capabilities, performance characteristics, and limitations. The system automatically handles model selection, failover, and optimization to provide the best experience for your use case. ### Model Compatibility Versions Model Compatibility Versioning identifies sets of LLMs compatible with specific service versions. This ensures your application continues to work reliably as new models are added or existing models are updated. Each service version has a specific set of compatible models, with older versions remaining available for existing integrations and new models added to newer service versions, providing clear upgrade paths. #### How It Works 1. **Versioned Model Sets** – Each compatibility version represents a curated list of models available at a specific API version. 2. **Stable Integrations** – Your application continues working even as new models are added to the platform. 3. **Controlled Upgrades** – Upgrade to newer compatibility versions when ready to test and support new models. 4. **Backward Compatibility** – Older versions remain supported, allowing you to upgrade on your schedule. #### Checking Compatibility You can check which models are available for your service version: ``` GET /v1/models/1 ``` Currently, there is only one compatibility version available: `1`. This version includes all currently supported models. As the platform evolves, new compatibility versions will be introduced to provide curated model sets for different API versions. Example response: ``` { "items": [ { "id": "gpt-4o", "name": "GPT-4o", "provider": "OpenAI", "allowed": false, "capabilities": { "webSearch": { "enabled": false }, "reasoning": { "enabled": false } } }, { "id": "gemini-2-5-pro", "name": "Gemini 2.5 Pro", "provider": "Google", "allowed": true, "capabilities": { "webSearch": { "enabled": true, "allowed": true }, "reasoning": { "enabled": true, "allowed": false } } } ] } ``` ### Model Capabilities All models support conversations, reviews, and actions. Some models also support web search and reasoning capabilities. #### Web Search Real-time web search with source attribution for current events, fact verification, and research. May increase response time. When enabled, models can search the web before generating responses to access up-to-date information beyond their training data. You can enable web search by including `"webSearch": {}` in the `capabilities` object of your API request. #### Reasoning A step-by-step reasoning for complex problem solving, mathematical reasoning, and logical analysis. Available for select models (support varies by model and compatibility version). **Important:** Some models have reasoning **always enabled** and cannot disable it. For these models: * The reasoning capability is always active during inference. * You cannot turn reasoning off via the API. To determine if a model has always-on reasoning, check the API response when listing models. Models with mandatory reasoning will indicate this in their capability structure. #### Web Scraping Extract and process content from web pages for analysis and summarization. ### Model Limitations #### File Processing Limits Files are limited to 7MB each (PDF, DOCX, PNG, JPEG, Markdown, HTML, Plain text). You can upload up to 100 files per conversation with a total size limit of 30MB. PDF files are limited to 100 pages total across all PDFs in a conversation. ##### Model-specific file limits Anthropic and agent models may use files up to 5MB each. #### Content Moderation All models include moderation for inappropriate content, harmful instructions, personal information, copyrighted material, misinformation, sensitive topics, and security threats. #### Model Descriptions Model descriptions returned by the API are provided in English and may be updated over time to reflect model improvements or capability changes. #### Translation and Localization If your application requires translated model descriptions, maintain a translation map in your code keyed by `model.id`, with fallback to the English description from the API for unknown models. This allows new models to work immediately while you add translations at your own pace. #### Model Deprecation Models scheduled for removal will include a `removal` field with an ISO 8601 date (e.g., `"removal": "2025-11-17T00:00:00.000Z"`). When a model is removed, API requests will fail with error code `MODEL_NOT_FOUND` and the models endpoint will stop returning that particular model. ### API Examples #### Model Selection ``` POST /v1/conversations/my-conversation-123/messages Content-Type: application/json Authorization: Bearer { "prompt": "Analyze this document and provide insights", "model": "agent-1", "content": [ { "type": "document", "id": "doc-1234567890123" } ] } ``` #### Capability Configuration ``` POST /v1/conversations/my-conversation-123/messages Content-Type: application/json Authorization: Bearer { "prompt": "Research the latest developments in AI", "model": "gpt-4o", "capabilities": { "webSearch": {}, "reasoning": {} } } ``` #### Model Information Get all available models for compatibility version `1`: ``` GET /v1/models/1 Authorization: Bearer ``` Response: ``` { "items": [ { "id": "agent-1", "name": "Agent", "provider": "Agent", "description": "Automatically selects the best model for speed, quality, and cost", "allowed": true, "capabilities": { "webSearch": { "enabled": true, "allowed": true }, "reasoning": { "enabled": true, "allowed": true } }, "limits": { "maxPromptLength": 30000, "maxConversationLength": 256000, "maxFiles": 100, "maxFileSize": 7000000, "maxTotalFileSize": 30000000, "maxTotalPdfFilePages": 100 } } ] } ``` ### API Reference For complete documentation on model endpoints, compatibility versions, and capability schemas, see: * **[Models API Reference](https://ai.cke-cs.com/docs#tag/Models)** – Full documentation for model listing and configuration. * **[Complete API Documentation](https://ai.cke-cs.com/docs)** – Interactive API reference with all CKEditor AI endpoints. ### Related Features * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – Use models in interactive AI discussions. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – Apply models to content analysis and improvement. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – Use models for content transformation tasks. source file: "cs/latest/guides/ckeditor-ai/overview.html" ## Overview CKEditor AI integrates AI-assisted authoring with rich-text editing. Users can interact through Actions, Reviews, or Conversations that can use relevant context from multiple sources. ### Getting Started New to CKEditor AI? Kick you adventure off with the [Quick Start guide](#cs/latest/guides/ckeditor-ai/quick-start.html) to set up your environment, generate access credentials, and make your first API call. ### CKEditor AI features * **[Conversations](#cs/latest/guides/ckeditor-ai/conversations.html)** – Interactive AI chats with history and persistent context. * **[Reviews](#cs/latest/guides/ckeditor-ai/reviews.html)** – Content analysis and proofreading, optimized for larger content. * **[Actions](#cs/latest/guides/ckeditor-ai/actions.html)** – Fast, stateless operations for specific tasks. * **[Document Processing](#cs/latest/guides/ckeditor-ai/document-processing.html)** — Send HTML + a natural-language instruction, get back the modified document and a summary of changes. Synchronous JSON. Experimental. * **[MCP support](#cs/latest/onpremises/ckeditor-ai-onpremises/mcp.html)** – Connect to MCP servers to extend AI capabilities with external tools. Available only in the [on-premises version](#cs/latest/onpremises/ckeditor-ai-onpremises/overview.html). ### Architecture The following pages cover the system architecture. * **[Models](#cs/latest/guides/ckeditor-ai/models.html)** – AI model selection, capabilities, and configuration. * **[Streaming](#cs/latest/guides/ckeditor-ai/streaming.html)** – Real-time streaming of AI-generated responses. * **[Permissions](#cs/latest/guides/ckeditor-ai/permissions.html)** – How to control user access to features. * **[Limits](#cs/latest/guides/ckeditor-ai/limits.html)** – Rate limits, context size limits, and file restrictions. ### Data Handling and Security #### Regional Data Storage All data stored by CKEditor AI follows the region settings of your CKEditor Cloud Services environment, ensuring compliance with data residency requirements and optimal performance for your geographic location. #### Data Retention Policy Conversation data is automatically deleted after 12 months of inactivity, including: * all conversation messages and history, * attached documents, files, and web resources, * conversation metadata and settings. #### Security All data is encrypted in transit and at rest with end-to-end encryption. Conversations and attachments are stored in secure cloud infrastructure with fine-grained access control and comprehensive permission systems. [Audit Logs](#cs/latest/guides/system-security.html--audit-logs) are available through our Customer Portal and the API. #### AI Interaction Review The SaaS version of CKEditor AI includes an AI interaction review setting that helps us improve response quality and diagnose issues. No customer data is ever used to train AI models. If this setting is enabled in the [Customer Portal](https://portal.ckeditor.com/), authorized CKEditor team members may review selected AI interactions for service improvement and troubleshooting. **What may be reviewed** Selected AI interactions may include prompts, responses, and related content submitted to CKEditor AI features as part of a request. CKEditor AI On-Premises does not allow sharing any data externally, so AI interaction review does not apply to On-premises deployments. ### On-Premises Deployment For organizations that need to run CKEditor AI within their own infrastructure, an on-premises version of the service is available. See the [CKEditor AI On-Premises documentation](#cs/latest/onpremises/ckeditor-ai-onpremises/overview.html) for details on deployment, configuration, and requirements. ### Resources and Support * **REST API Documentation**: – Complete API reference for CKEditor AI. * **Customer Support**: [Contact us](https://ckeditor.com/contact/) to get help from our support team or speak with sales. source file: "cs/latest/guides/ckeditor-ai/permissions.html" ## Permissions You can control access to AI features, models, and capabilities based on user roles, subscription tiers, and organizational requirements. ### Overview The CKEditor AI permission system allows integrators to manage their users’ access to specific functionality. This enables control over AI features, models, and capabilities based on user roles, subscription tiers, or organizational requirements. Permissions provide flexible access control, role-based management, security, and cost control for premium models and capabilities. ### Use Cases For SaaS applications, users in different tiers might have access to different features. You can upgrade/downgrade users between tiers while preserving their data, control file type access where higher tiers can use more file types, manage model access where premium tiers can use more powerful models, and implement feature restrictions where basic tiers have limited functionality. For enterprise systems with multiple roles, you can implement file upload restrictions to prevent certain roles from uploading confidential information, provide review-only access for some users, disable manual model selection for users unfamiliar with AI differences, and grant feature-specific access for editors, reviewers, and administrators. ### Permission Format Permissions are specified in the JWT token’s `auth.ai.permissions` claim as an array of permission strings. Each permission follows the format: `ai::` #### Admin Permissions ##### `ai:admin` Grants full administrative access to the AI. #### Model Permissions Model permissions control which AI models users can access across all features. ##### `ai:models:*` Wildcard permission grant access to all available models. This is the default setting for most users, but new, potentially expensive models become automatically available. ##### `ai:models::*` Access to all models from a specific provider. Examples: `ai:models:openai:*`, `ai:models:anthropic:*`, `ai:models:google:*` ##### `ai:models::` Access to a specific model from a provider. Examples: `ai:models:openai:gpt-5`, `ai:models:anthropic:claude-4-sonnet` ##### `ai:models:agent` ⭐ **Recommended** Allow users to use the agent-1 model for optimal performance. No need to manually select models; automatic optimization for performance and cost. #### Conversation Permissions ##### `ai:conversations:*` Wildcard permission for all conversation-related features, including all conversation permissions below. ##### `ai:conversations:read` Access to chat interface and conversation history, including loading conversation lists, viewing messages, and accessing chat UI. ##### `ai:conversations:write` Ability to create and modify conversations, including sending messages and creating/updating/deleting conversations. Requires both `read` and `write` permissions for full functionality. ##### `ai:conversations:websearch` Enable web search capability in conversations. ##### `ai:conversations:reasoning` Enable reasoning capability in conversations. #### Context Permissions Context permissions control which types of content can be attached to conversations. ##### `ai:conversations:context:*` Access to all context sources, including all file types, web resources, and other context types. ##### `ai:conversations:context:files:*` Access to all supported file types, including PDF, DOCX, TXT, MD, PNG, JPEG, HTML. ##### `ai:conversations:context:files:` Access to specific file formats. Examples: * `ai:conversations:context:files:pdf` * `ai:conversations:context:files:docx` * `ai:conversations:context:files:png` * `ai:conversations:context:files:jpg` ##### `ai:conversations:context:urls` Ability to add web URLs as context. #### Actions Permissions ##### `ai:actions:*` Access to all action types, including custom and system actions. ##### `ai:actions:custom` Ability to run custom actions with free-form prompts. ##### `ai:actions:system:*` Access to all pre-defined system actions. ##### `ai:actions:system:` Access to specific system actions. Examples: * `ai:actions:system:improve-writing` * `ai:actions:system:fix-grammar` * `ai:actions:system:translate` #### Reviews Permissions ##### `ai:reviews:*` Access to all review types, including custom and system reviews. ##### `ai:reviews:custom` Ability to run custom reviews with free-form prompts. ##### `ai:reviews:system:*` Access to all pre-defined system reviews. ##### `ai:reviews:system:` Access to specific system reviews. Examples: * `ai:reviews:system:correctness` * `ai:reviews:system:clarity` * `ai:reviews:system:make-tone-professional` ### Permission Examples #### Basic User ``` { "auth": { "ai": { "permissions": [ "ai:conversations:read", "ai:conversations:write", "ai:models:agent", "ai:conversations:context:files:pdf", "ai:conversations:context:files:docx" ] } } } ``` #### Premium User ``` { "auth": { "ai": { "permissions": [ "ai:conversations:*", "ai:models:*", "ai:actions:system:*", "ai:reviews:system:*" ] } } } ``` #### Enterprise Admin ``` { "auth": { "ai": { "permissions": [ "ai:admin" ] } } } ``` #### Restricted User (Review Only) ``` { "auth": { "ai": { "permissions": [ "ai:reviews:system:correctness", "ai:reviews:system:clarity", "ai:models:gpt-4.1-mini" ] } } } ``` ### Best Practices #### Permission Design Begin with minimal, specific permissions based on actual requirements. Use wildcards only for testing environments and power users who need comprehensive access. Gradually expand permissions based on user needs and usage patterns. Avoid `ai:models:*` in production to prevent unexpected access to new expensive models. Use provider-specific permissions like `ai:models:openai:*` for better control, or specify exact models for maximum control. Start with common formats (PDF, DOCX, TXT, PNG, JPEG) and add specialized formats only when needed. ### Error Handling When a user lacks required permissions, the API returns a `403 Forbidden` error with the message “No permissions to the resource”. Common issues include missing model permissions, file type restrictions, feature access without permission, and action/review access without permission. ### Next Steps * [Learn about AI Models](#cs/latest/guides/ckeditor-ai/models.html) for model selection and capabilities. * [Learn about Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) for interactive AI discussions. * [Explore Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) for content improvement. * [Discover Actions](#cs/latest/guides/ckeditor-ai/actions.html) for content transformation. * [API Documentation](https://ai.cke-cs.com/docs) – Complete API reference for CKEditor AI. source file: "cs/latest/guides/ckeditor-ai/quick-start.html" ## Quick Start The aim of this article is to get you up and running with CKEditor AI. To start, follow the steps below: * Sign up for one of our [self-service plans](https://ckeditor.com/pricing/), or [contact us](https://ckeditor.com/contact/) to purchase the CKEditor AI license. * Generate your access credentials in the [Customer Portal](https://portal.ckeditor.com/). * Write a script that generates one-time tokens for authorizing end users of your application in CKEditor Cloud Services (using access credentials created earlier). All steps are explained in details below. ### Get the CKEditor AI license You can purchase CKEditor AI as an add-on to selected tiers of our self-service plans. You can find details on our [pricing page](https://ckeditor.com/pricing/). You can also [contact us](https://ckeditor.com/contact/) directly if you would like to have a custom plan. For testing purposes, you can sign up for the [free trial](https://portal.ckeditor.com/checkout?plan=free). After signing up, you will receive access to the customer dashboard (Customer Portal). ### Log in to the Customer Portal Log in to the [Customer Portal](https://portal.ckeditor.com) and navigate to “Cloud environments”. ### Create token endpoint You now need to create a [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) in your application. This endpoint securely authorizes end users of your application to use CKEditor AI features based on their permissions and access rights. #### Development token endpoint If you are just starting, you may use the development token endpoint URL which is available out of the box and requires no coding on your side. The development token URL can be obtained in two simple steps: 1. From the list of environments, select the one you want to manage. To create a new environment, follow the [Environments management](#cs/latest/developer-resources/environments/environments-management.html--creating-a-new-environment) guide. 1. The development token URL is accessible at the bottom of this section: ##### Simulating specific users The development token URL generates random user data by default from a pool of 10 predefined users. If you would like to specify a particular user for AI functionality, you may pass the user ID in the query string using: * `sub` (Optional) – The unique ID of the user in your system. **Example:** If your token URL is `https://17717-dev.cke-cs.com/token/dev/XXX`, you can connect as user with ID “13” using: `https://17717-dev.cke-cs.com/token/dev/XXX?sub=13` ##### Extending the default user limit You may need to extend the default limit of 10 development users. Add a `limit` query parameter to your dev token URL: **Example:** ``` https://17717-dev.cke-cs.com/token/dev/XXX?limit=40 ``` This URL provides tokens from a pool of 40 predefined users. After the 10th token, you will start paying for development users. The maximum allowed value for the limit parameter is 50, so the maximum number of paid users will be 40 with predefined users. Using your own generated user IDs may result in an unpredictable number of paid users. #### Writing your own token endpoint To write your own [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html), create access credentials for the selected environment by going to the “Access credentials” tab and clicking the “Create a new access key” button. Read more in the [Creating access credentials](#cs/latest/developer-resources/security/access-key.html--creating-access-credentials) section of the Environments management guide. ### Cloud region Cloud Services can [reside in either US or EU region or in both](#cs/latest/guides/saas-vs-on-premises.html--cloud-region). The region is set per subscription and cannot be changed for existing environments by the user. For Custom plan with multi-region, you can choose the region during environment creation. This topic is addressed in more detail in the [Environment management](#cs/latest/developer-resources/environments/environments-management.html--cloud-region) guide. ### API Integration All features are accessible through the REST API at `https://ai.cke-cs.com` with JWT authentication. For detailed API examples and implementation guides, see: * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – Interactive AI discussions and document analysis. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – Content improvement and quality analysis. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – Content transformation and batch processing. * [Streaming](#cs/latest/guides/ckeditor-ai/streaming.html) – Real-time AI interactions. [Complete API Documentation](https://ai.cke-cs.com/docs) – Full API reference for CKEditor AI ### Next Steps Now that you made your first API call, explore the features: * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – Start with interactive AI discussions. * [AI Models](#cs/latest/guides/ckeditor-ai/models.html) – Choose the right model for your use case. * [Permissions](#cs/latest/guides/ckeditor-ai/permissions.html) – Set up user access control for production. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) – Add content improvement features. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – Implement content transformation. ### Integration with CKEditor 5 CKEditor 5 includes built-in support for CKEditor AI features. To enable AI capabilities in your editor: 1. **Install the AI plugin** – Add the CKEditor AI plugin to your CKEditor 5 build 2. **Configure credentials** – Provide your Cloud Services token endpoint 3. **Customize features** – Choose which AI features to enable (conversations, reviews, actions) 4. **Set permissions** – Control user access through JWT token permissions For detailed integration instructions, see the [CKEditor 5 AI Features documentation](#ckeditor5/latest/features/ai/ckeditor-ai-overview.html). source file: "cs/latest/guides/ckeditor-ai/reviews.html" ## Reviews Analyze large documents for quality improvements and professional standards. Reviews analyze your content and provide specific recommendations for grammar, style, clarity, and tone improvements. **When to use Reviews vs Actions:** Use Reviews when you need to analyze entire documents for quality improvements and get suggestions without automatically changing the content. Use Actions when you need to transform specific text content (fix grammar, translate, adjust tone). ### Available Reviews #### System Reviews CKEditor AI provides built-in system reviews for comprehensive content analysis: * **Correctness** – Fix grammar, spelling, and factual errors. * **Clarity** – Improve sentence structure, word choice, and logical flow. * **Readability** – Enhance paragraph structure, transitions, and reading level. * **Length Optimization** – Expand or condense content while preserving key information. * **Tone Adjustment** – Modify tone to casual, direct, friendly, confident, or professional styles. * **Translation** – Translate content between languages with cultural adaptation. #### Custom Reviews In addition to system reviews, you can create custom reviews tailored to your specific content quality standards and editorial guidelines. Custom reviews allow you to define specialized analysis criteria using your own prompts to control the review behavior. Unlike system reviews that use predefined identifiers, custom reviews use a unified endpoint where you define the analysis behavior through a prompt parameter. ### Key Features Reviews use streaming output with Server-Sent Events for real-time feedback as suggestions are generated. Each review type is optimized for specific improvement tasks, providing consistent, high-quality analysis of text structure, style, and quality. Reviews provide specific, actionable recommendations for content improvement. ### API Examples #### Basic Grammar Review Example ``` POST /v1/reviews/system/correctness/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

The norhtern lights dence across the polar skies, painting ribbons of green and purple light that ripple like a cosmic curtain.

" } ] } ```
#### Clarity Improvement Example ``` POST /v1/reviews/system/clarity/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

The system works by processing data through various algorithms to produce results.

" } ] } ```
#### Tone Adjustment Example ``` POST /v1/reviews/system/make-tone-casual/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

We regret to inform you that your request cannot be processed at this time.

" } ], "args": { "language": "casual" } } ```
#### Translation Review ``` POST /v1/reviews/system/translate/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

Hello, how are you today?

" } ], "args": { "language": "Spanish" } } ```
#### Custom Review Example ``` POST /v1/reviews/custom/calls Content-Type: application/json Authorization: Bearer { "content": [ { "type": "text", "content": "

Our product is really good and customers love it because it has many features.

" } ], "prompt": "Review the text for vague language and generic claims. Suggest specific, concrete alternatives that would make the content more credible and informative.", "model": "agent-1" } ``` Custom reviews require the `ai:reviews:custom` permission in your JWT token.
### Streaming Responses Reviews use Server-Sent Events (SSE) for real-time streaming results. See the [Streaming Responses guide](#cs/latest/guides/ckeditor-ai/streaming.html) for detailed implementation information. ### API Reference For complete endpoint documentation, request/response schemas, authentication details, and additional parameters, see: * **[Reviews API Reference](https://ai.cke-cs.com/docs#tag/Reviews)** – Full documentation for system and custom reviews endpoints. * **[Complete API Documentation](https://ai.cke-cs.com/docs)** – Interactive API reference with all CKEditor AI endpoints. ### Related Features * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) – For interactive discussions with document analysis and context. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html) – For content transformation and batch processing. * [Streaming Responses](#cs/latest/guides/ckeditor-ai/streaming.html) – For implementing real-time review suggestions. source file: "cs/latest/guides/ckeditor-ai/streaming.html" ## Streaming Real-time AI interactions using Server-Sent Events (SSE) for immediate feedback and progressive content generation. ### Overview CKEditor AI services use Server-Sent Events (SSE) to provide real-time streaming responses. This allows you to see AI-generated content as it is being created, providing immediate feedback and enabling interactive experiences. ### SSE Event Types Different AI services provide different types of streaming events. For service-specific event details, see: * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html--streaming-responses) – Interactive AI discussions with text streaming, web search sources, and reasoning. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html--streaming-responses) – Content improvement suggestions and review progress. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html--streaming-responses) – Content transformations and action progress. ### Basic Implementation Here is the standard pattern for consuming SSE streams: ``` const response = await fetch('/v1/your-endpoint', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ // Your request payload }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); // Handle different event types // See service-specific guides for detailed event handling: // - Conversations: text-delta, source, reasoning, modification-delta // - Reviews: review-delta, review-metadata // - Actions: modification-delta, action-metadata switch (data.event) { case 'error': // Handle errors console.error('Error:', data.data.message); break; default: // Handle all other events console.log('Event:', data.event, data.data); } } } } ``` ### Event Handling Patterns For detailed event handling examples specific to each service, see: * [Conversations](#cs/latest/guides/ckeditor-ai/conversations.html--streaming-responses) – Text streaming, web search sources, reasoning, and document modifications. * [Reviews](#cs/latest/guides/ckeditor-ai/reviews.html--streaming-responses) – Review suggestions and progress tracking. * [Actions](#cs/latest/guides/ckeditor-ai/actions.html--streaming-responses) – Content transformations and action progress. ### Error Handling Always handle errors gracefully: ``` if (data.event === 'error') { const error = data.data; console.error('Streaming error:', error.message); // Show user-friendly error message showErrorMessage(error.message); // Optionally retry or fallback if (error.retryable) { setTimeout(() => retryRequest(), 1000); } } ``` ### Progress Tracking Use metadata events to show progress. For service-specific progress tracking examples, see [Review progress and status information](#cs/latest/guides/ckeditor-ai/reviews.html). ### API Reference For complete documentation on streaming endpoints, event schemas, and error codes, see: * **[Complete API Documentation](https://ai.cke-cs.com/docs)** – Interactive API reference with streaming implementation details. * **[Conversations API](https://ai.cke-cs.com/docs#tag/Conversations)** – Streaming events for conversations. * **[Reviews API](https://ai.cke-cs.com/docs#tag/Reviews)** – Streaming events for reviews. * **[Actions API](https://ai.cke-cs.com/docs#tag/Actions)** – Streaming events for actions. ### Next Steps * [Learn about Conversations](#cs/latest/guides/ckeditor-ai/conversations.html) for interactive AI discussions. * [Explore Reviews](#cs/latest/guides/ckeditor-ai/reviews.html) for content improvement. * [Discover Actions](#cs/latest/guides/ckeditor-ai/actions.html) for content transformation. source file: "cs/latest/guides/collaboration/access-control.html" ## Access control ### Overview Depending on the application’s use case, a need may occur to restrict access to particular capabilities of working with the documents. The CKEditor Collaboration Server provides a feature which allows to set specific privileges to a specific document for a particular user. The feature is a part of the [token](#cs/latest/developer-resources/security/token-endpoint.html--token-payload) returned from the [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html). Thanks to that, the privileges are fully controlled by the application. To make the managing of basic privileges easier, the CKEditor Collaboration Server provides three predefined roles: `reader`, `commentator` and `writer`. The roles have been described in more detail in the dedicated [roles section](#cs/latest/developer-resources/security/roles.html--roles) of the Security guide. In case of need to create more specific roles, the CKEditor Collaboration Server allows to set particular privileges, which have been described in the [Roles and permissions](#cs/latest/developer-resources/security/roles.html--permissions) guide. Keep in mind that privileges and roles are set separately for each user, so the logic of picking the right role or privileges must be implemented as a part of the [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) inside of the backend application. ### Examples #### Restricting document editing Let’s assume there is a need to allow the user with ID `user284` to only read documents named with the `-read` suffix. However, they should also be able to edit documents with the `-edit` suffixes. The example below shows what the [token](#cs/latest/developer-resources/security/token-endpoint.html--token-payload) should look like for such a case: ``` { aud: 'your_environment_id', sub: 'user284', auth: { collaboration: { '*-read': { 'permissions': [ 'document:read', 'comment:read' ] }, '*-edit': { 'permissions': [ 'document:read', 'comment:read', 'document:write', 'comment:write' ] } } } } ``` In the example above, the `*-read` and `*-edit` properties represent patterns of document/channel IDs, so for the document/channel with ID `my-document-read`, the user with ID/sub `user284` will be able only to read the document. However, for a document/channel with ID `my-document-edit` they will also be able to edit it. With these permissions, the user with ID `user284` will not be able to open any other document than the documents with matching `*-edit` or `*-read` document/channel ID patterns. #### Removing comment threads There can also arise a need to create a user with privileges to remove whole comment threads created by other users in the whole environment. The user with ID `user284` should be able to add comments as well as remove whole comment threads from a document, but should not be able to edit document content. ``` { aud: 'your_environment_id', sub: 'user284', auth: { collaboration: { '*': { 'permissions': [ 'document:read', 'comment:read', 'comment:write', 'comment:admin' ] } } } } ``` In the presented example, the user with ID `user284` can read all documents in the environment and comment them. Moreover, the user has the privilege to remove comment threads created by any other user, but they still cannot edit nor remove particular comments. ### Next steps [Read how to implement, configure and use the token endpoint](#cs/latest/developer-resources/security/token-endpoint.html). [Read complete guide on how to use user roles and privileges](#cs/latest/developer-resources/security/roles.html). source file: "cs/latest/guides/collaboration/connection-optimization.html" ## Connection optimization ### Overview A need for optimization may arise in documents that are being edited for a long time. The optimization is based on compressing operations sent during the editing process. This speeds up the document loading time in the rich text editor. CKEditor 5 performs lots of calculations during the processing of operations. This affects the document loading time because the browser resources are limited. Compressing operations on connecting means that instead of processing thousands of operations, the editor only needs to process a few of them. ### Prerequisites There are two steps that need to be taken, before you can start using the feature: 1. Upload your editor bundle with the editor configuration to the CKEditor Cloud Services server. Refer to the [Editor bundle](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle) guide for more information. 2. Set the required [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion) property in the editor configuration. Refer to the [Editor configuration](#cs/latest/guides/collaboration/editor-bundle.html--editor-configuration) section of the editor bundle documentation for more details. ### Enabling the connection optimization feature Follow the steps below to set up the connection optimization feature. 1. Log in to the [Customer Portal](https://portal.ckeditor.com) and navigate to “Your products > Cloud Services”. 2. From the list of available subscriptions in the dashboard choose the subscription that you want to manage and press the “Manage” link. 1. Select the environment where you want to enable the connection optimization feature. 1. Navigate to the “Feature configuration” tab. 1. Switch on the feature in the “Collaboration > Connection optimization” section. ### Usage After you complete the steps above, the connection optimization feature will work automatically. CKEditor Cloud Services will periodically compress the operations, lowering the document loading time for your users. ### Debugging If needed, it is possible to store all the collaboration data for debugging purposes. Thanks to this, our customer support team will be able to check the errors related to the connection optimization feature. However, please enable this option only when you are asked to do so by our customer support team. source file: "cs/latest/guides/collaboration/connection-troubleshooting.html" ## Connection troubleshooting ### Overview The CKEditor Collaboration Server utilizes WebSockets connections to communicate with the CKEditor. WebSockets connections provide two-way communication between the editor and server. Such a connection is used to provide a real-time experience. Another way CKEditor 5 communicates with external resources is by HTTPS connection. This mechanism is used to fetch a token from a token endpoint and upload and download images from CKBox service. ### Requirements * WebSockets (port 443) * HTTPS (port 443) ### Possible problems #### Restricted WebSocket connection In case when the WebSocket connection is not possible, the user will receive an error with `The number of initial connection attempt exceeded.` message. In such a case, please verify whether the CKEditor Collaboration Server is available from the user’s browser. It can be done by opening the default page of the server: for SaaS visit [the Info Page](https://cke-cs.com/) page, for On-premises visit the `https://{your_domain}/panel` page and from the On-premises in version `4.5.0` the `https://{your_domain}/` page. If the page is available but the error still occurs then there is a possibility that the WebSocket connection in blocked on the user’s device or network level. The WebSocket connection may be blocked in a few ways: User level: * antivirus software * firewalls * older browsers or outdated browser versions Network level: * firewall * router configuration * Internet provider restrictions Please verify that none of above things blocks the WebSocket connection. It is possible you will need to contact your system and network administrator. If the problem with WebSocket connection still occurs, feel free to [contact us](https://ckeditor.com/contact/). source file: "cs/latest/guides/collaboration/data-deletion.html" ## Data deletion ### Overview There are a few ways to different delete data from the server: * [Collaboration REST API](https://help.cke-cs.com/api/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete) to delete only active collaboration session data. * [Storage REST API](https://help.cke-cs.com/api/docs#tag/Storage/paths/~1storage~1%7Bdocument%5Fid%7D/delete) to delete document content from the document storage. * [Suggestions REST API](https://help.cke-cs.com/api/v5/docs#tag/Suggestions/paths/~1suggestions/delete) to delete comments. * [Comments REST API](https://help.cke-cs.com/api/v5/docs#tag/Comments/paths/~1comments/delete) to delete suggestions. ### Document delete As you can see, all the above request deletes only part of the entire document. Depending on your needs, you may want to delete the entire document with all comments, suggestions, and more. To do this you can use [Documents REST API](https://help.cke-cs.com/api/docs#tag/Documents). #### Example The example below has been prepared in `Node.js`. 1. Run the following commands ``` mkdir cs-document-delete-example && cd cs-document-delete-example && npm init -y && npm i axios && touch delete.cjs ``` 1. Open `cs-document-delete-example/delete.cjs` and paste the following code snippet: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; // Set document id const documentId = 'my_document_id'; const deleteApiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/documents/${ documentId }`; ( async () => { await deleteDocument(); async function deleteDocument( ) { try { const config = getConfig( 'DELETE', deleteApiEndpoint ); const response = await axios.delete( deleteApiEndpoint, config ); console.log( response.status ); } catch ( error ) { console.log( error.message ); console.log( error.response.data ); } } } )(); function getConfig( method, url ) { const CSTimestamp = Date.now(); return { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, method, url, CSTimestamp ) } }; } function generateSignature( secret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', secret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); } return hmac.digest( 'hex' ); } ``` 1. Update your credentials, and the `documentId` value in the code snippet with the id of an existing document. 2. Execute the `node delete.cjs` command. After a successful response with a status code `204` from the delete request, the document with all resources will be deleted from the server. ### Troubleshooting * We suggest enabling [Insight Panel](#cs/latest/developer-resources/monitoring/insights-panel.html), where you will find detailed logs from the whole process including exact reasons for any failed requests. source file: "cs/latest/guides/collaboration/data.html" ## Collaboration data ### Overview The collaboration session starts when the first user connects to the document. The content is stored for as long as there is at least one user connected to the document. When the last user disconnects, the server will wait for 24 hours and then delete the temporary collaboration data. One of the steps of integrating CKEditor 5 and Real-Time Collaboration with your application is providing saving the capabilities of the collaboration data to some permanent storage location and initializing the editor with new or saved content. There are several solutions to this. This guide will cover the available scenarios for getting and setting the collaboration data. ### Temporary and permanent data **CKEditor Cloud Services remove the collaboration session data 24 hours after the last user disconnects.** This includes any content in the editor that was created by the users. To preserve your content between collaboration sessions, you need to save it on your server or use our [Document storage](#cs/latest/guides/collaboration/document-storage.html) feature. Only operations performed on the document are stored during collaboration session lifetime. Operation is every update (document history) made during the editing session stored as an [object model](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fengine%5Fmodel%5Foperation%5Foperation-Operation.html). Some of the data is stored permanently. This includes comments, suggestions, revisions, and user data. Thanks to this, comment content, timestamps, suggestion metadata, and user details will be stored between collaboration sessions and the users are not able to modify other users’ data. When it comes to data removal, document content is permanently removed, while comments and suggestions are only marked as removed (e.g. when removing a comment or accepting a suggestion). The REST API for [comments](https://help.cke-cs.com/api/v5/docs#tag/Comments) and [suggestions](https://help.cke-cs.com/api/v5/docs#tag/Suggestions) also provides two types of removal - soft (the default one) and permanent (requires an explicit flag). Deleting the environment permanently removes all the data associated with this environment. ### Recovery snapshots When an editing session ends, a collaboration snapshot is created. It includes all the necessary data for debugging and retrieval of the document when required. The snapshot data is encrypted with a key unique for each environment. Snapshots and related data are permanently removed after 30 days. #### Use cases The collaboration snapshots feature can be helpful when solving hard to reproduce cases related to documents. For example: * When data is stored on the user’s side relying on webhooks. When issue occurs with webhook delivery the data might be potentially lost. * Issues related to the use of erroneous editor bundle. * Issues related to operation corrupting the document. * Other cases introducing irreversible changes to the document. #### Recovery steps In order to debug and/or retrieve document, the CKEditor team needs you to provide a zip containing these 3 files: * The document snapshot `.json` * The [editorBundle](#cs/latest/guides/collaboration/editor-bundle.html) * The [config](#cs/latest/guides/collaboration/editor-bundle.html--editor-configuration) To retrieve your document’s snapshot follow these steps: 1. Get a list of snapshots existing for the selected document. Use REST API [GET metadata endpoint](https://help.cke-cs.com/api/v5/docs#tag/Collaboration-snapshots/paths/~1collaboration-snapshots~1%7Bdocument%5Fid%7D~1metadata/get). 2. Use selected snapshot’s metadata to retrieve it and save it as `.json`. Use REST API [GET snapshots endpoint](https://help.cke-cs.com/api/v5/docs#tag/Collaboration-snapshots/paths/~1collaboration-snapshots~1%7Bdocument%5Fid%7D/get). 3. Get editorBundle. Use REST API [Get editor bundle endpoint](https://help.cke-cs.com/api/v5/docs#tag/Editor-bundles/paths/~1editors~1%7Bbundle%5Fversion%7D/get). 4. Get config. Use REST API [Get editor bundle config endpoint](https://help.cke-cs.com/api/v5/docs#tag/Editor-bundles/paths/~1editors~1%7Bbundle%5Fversion%7D~1config/get). 5. Send editorBundle, config and snapshot to our team. When the CKEditor team retrieves operations from your document you will receive a file with your document’s content in HTML. Use the retrieved data to create [new editing session](#cs/latest/guides/collaboration/import-and-export.html--initiating-the-collaboration-session) by sending request to this [REST API endpoint](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations/post). If you have any feedback regarding this feature, feel free to [contact us.](https://ckeditor.com/contact/) ### Comments and suggestions When a user adds a new comment or suggestion, in the editor’s content you will only find a reference to it – two HTML elements marking the start and the end of the suggestion or comment with an additional ID attribute. The rest of the data (the creation time, the user name, the content of the comment, etc.) is not visible in the editor data. In the case of Real-Time Collaboration plugins, the comments or suggestions metadata is sent to the Cloud Services server, where it can be securely stored and shared only with authorized users. Thanks to this approach, users cannot modify other users’ comments and suggestions. Any comment or suggestion created during the collaboration session is assigned to the [document ID](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html--the-channelid-configuration-property), where it was created. You cannot access its data with a different document ID. Similarly, you cannot access comments or suggestions created in a different environment, even if they are from the same organization or are on the same Collaboration Server On-Premises instance. If you open a document containing comments or suggestions created in a collaboration session with a different `documentId` or in a different environment, it can result in an editor crash with one of the following errors: * `track-changes-user-not-found` * `Cannot find suggestion abc` * `Comment thread abc not found` The accepted or rejected suggestions and deleted comments are only marked as removed. This means that they can be restored via the REST API even after the collaboration session ends (24 hours after the last user disconnects). All restored metadata will be kept in the database as well. It is also possible to delete the comments and suggestions permanently using the REST API. ### Comment archive The comment archive feature requires storing additional data (context, attributes, etc.) of comment threads. Additional comment thread properties can be synchronized via comments REST-API, documents REST-API, or webhooks. Starting with the v37.0.0 release of CKEditor 5 the comment thread data handling was slightly changed. Accepted comment threads are now treated as resolved and moved to the comment archive. Therefore, instead of `commentthread.removed` a new webhook called `commentthread.resolved` is triggered when a comment is moved to the archive. When residing in an archive, a comment thread can be either: * reopened – in such a case the `commentthread.reopened` webhook is triggered * removed – in such a case the old `commentthread.removed` webhook is triggered Moreover, 2 new webhooks were added: * `commentthread.added` – triggered when comment thread is added, can contain added comments * `commentthread.updated` – triggered when the comment thread is updated #### Rest API You can use Cloud Services REST API for CRUD operations on suggestions and comments. Example use cases of the suggestions and comments CRUD operations: * transferring your existing content into Cloud Services databases, * moving the suggestions or comments data between environments, * moving the suggestions or comments data from one document to another, * adding attributes to the suggestions or comments. * … and more. Refer to the [REST API documentation](https://help.cke-cs.com/api/v5/docs#tag/Comments) for more details. #### Webhooks CKEditor Cloud Services sends an HTTP POST request to a configured URL when specified [events](#cs/latest/developer-resources/webhooks/events.html) are triggered. You can use comments and suggestions webhooks to: * sync data from CKEditor Cloud Services servers with your databases, * notify users when their comment is removed or gets an answer, * notify users when the state of their suggestions changes, * … and more. Refer to the [Webhooks documentation](#cs/latest/developer-resources/webhooks/overview.html) for more details. ### Data safety The data stored permanently by CKEditor Cloud Services is encrypted with a key that differs for each environment. Additionally, in most cases, the system uses identifiers instead of the data that originates from the users. CKEditor Cloud Services uses encryption to protect the communication between the user and the server and provides all communication interfaces using HTTPS or WSS protocols. Refer to the [system security](#cs/latest/guides/system-security.html) section for more details. source file: "cs/latest/guides/collaboration/document-copy.html" ## Document copy ### Overview The document copy feature allows users to copy an entire document from an active collaborative editing session or from [the document storage](#cs/latest/guides/collaboration/document-storage.html). The copy of the document can be then edited independently from the original document but will still contain all the comments and suggestions from the source document. ### Prerequisites * An [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) needs to be uploaded. * The source document needs to be created after uploading the editor bundle. ### Usage Copying a document means creating a new collaborative editing session using the document data from an already existing one on the CKEditor Collaboration Server. It is done by sending a request to `POST /documents/{source_document_id}` [method](https://help.cke-cs.com/api/docs#tag/Documents/paths/~1documents~1%7Bsource%5Fdocument%5Fid%7D/post) from our [REST API](https://help.cke-cs.com/api/docs). You can also use the endpoint to quickly create a copy of the document with comment and suggestion markers excluded without the need to modify the original document. ### Example The example below has been prepared in `Node.js`. 1. Run the following commands ``` mkdir cs-copy-example && cd cs-copy-example && npm init -y && npm i axios && touch copy.js ``` 1. Open `cs-copy-example/copy.js` and paste the following code snippet: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; const apiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/documents/${ sourceDocumentId }`; // Set source and target document id const sourceDocumentId = 'my_document_id_source'; const targetDocumentId = 'my_document_id_target'; const body = { 'target_document_id': targetDocumentId, 'omit_comments': false, 'omit_suggestions': false }; const CSTimestamp = Date.now(); const config = { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, 'POST', apiEndpoint, CSTimestamp, body ) }, }; axios.post( apiEndpoint, body, config ) .then( response => { console.log ( response.status ); } ).catch( error => { console.log( error.message ); console.log( error.response.data ); } ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); }; return hmac.digest( 'hex' ); } ``` 1. Update your credentials, the `sourceDocumentId` and the `targetDocumentId` values in the code snippet. 2. Execute the `node copy.js` command. After a successful response with a status code `201`, you can open the editor with the same `targetDocumentId` as set in the snippet to see your copied content. ### Troubleshooting * In case of any operation while copying fails, e.g. fail while copying comments all data of a target document will be revoked. The data of the source document are not changed. * We suggest enabling [Insight Panel](#cs/latest/developer-resources/monitoring/insights-panel.html), where you will find detailed logs from the copy process including exact reasons for any failed requests. ### More info You will find more info in our [REST API documentation](https://help.cke-cs.com/api/docs#tag/Documents/paths/~1documents~1%7Bsource%5Fdocument%5Fid%7D/post). source file: "cs/latest/guides/collaboration/document-import-and-export.html" ## Document import and export ### Overview The document import/export feature allows users to export, and import entire documents including: * active collaborative editing session or stored content (if no active collaborative session exists and document storage is enabled), * comments, * suggestions, * revision history. ### Document import/export vs. collaboration import/export There is a similar [collaboration import and export](#cs/latest/guides/collaboration/import-and-export.html) feature which allows to import and export the data from the server. The main difference between those two is the fact that the document import/export allows for treating the document as one single resource providing the content, suggestions, comments, and more. The main benefit of this approach is the reduction of the number of requests that one has to perform to initialize and export all document data from the server. The second difference is that the [collaboration import and export](#cs/latest/guides/collaboration/import-and-export.html) feature only operates on an active collaboration session which means the data from the document storage will not be exported. What is more, the document export feature allows obtaining document content without the suggestions/comments markers. For example, the document export/import workflow may be the following: 1. Import the document data or create a new collaborative editing session by connecting CKEditor 5. 2. Collaborate on the document – add and modify content, comments, and suggestions. 3. Export document data – save the exported document data in a client’s storage. 4. Remove document data. ### Export prerequisites * An [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) needs to be uploaded. * The exported document needs to exist in an active collaboration session or document storage. ### Export usage Exporting the entire document means returning the data from the active collaborative editing session or the document storage (if present), as well as all document-related data like comments and suggestions. It is done by sending a request to `GET /documents/{document_id}` from our [REST API](https://help.cke-cs.com/api/docs#tag/Documents). You can also use the feature to quickly export the content with the suggestion or comment markers excluded without the need to modify the original document. ### Import prerequisites * An [editor bundle](#cs/latest/guides/collaboration/editor-bundle.html) needs to be uploaded. * The target document should exist neither in a collaboration session nor in the document storage. * Related comments and suggestions should not exist in the database. ### Import usage Importing the entire document means creating a new collaborative editing session using the document data from the request body including collaborative editing session data, comments, and suggestions. It is done by sending a request to `POST /documents` [REST API](https://help.cke-cs.com/api/docs#tag/Documents). ### Example The example below has been prepared in `Node.js`. 1. Run the following commands ``` mkdir cs-import-export-example && cd cs-import-export-example && npm init -y && npm i axios && touch importExport.cjs ``` 1. Open `cs-import-export-example/importExport.cjs` and paste the following code snippet: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; // Set document id const documentId = 'my_document_id'; const importApiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/documents`; const exportApiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/documents/${ documentId }`; const importBody = { id: documentId, content: { bundle_version: '34.1.0', data: '

Hello world from example suggestion CKEditor 5!

' }, comments: [ { id: 'e52d6b88056950a3d98ecd4f10053a74c', document_id: documentId, thread_id: 'e712d8a9d63d6f1201576ccb7c0f15a77', content: '

example comment

', attributes: {}, updated_at: '2022-09-28T18:11:47.258Z', created_at: '2022-09-28T18:11:47.239Z', user: { id: 'user-1' }, type: 1 } ], suggestions: [ { id: 'ed7c304a37913421ddc1d7950a144d41d', document_id: documentId, has_comments: false, attributes: {}, created_at: '2022-09-28T18:11:23.726Z', author_id: 'user-1', state: 'open', type: 'insertion', requester_id: 'user-1' } ] }; ( async () => { await importDocument( importBody ); const exportedData = await exportDocument(); console.log( 'Exported data:' ); console.log( exportedData ); async function importDocument( body ) { try { const config = getConfig( 'POST', importApiEndpoint, body ); const response = await axios.post( importApiEndpoint, body, config ); console.log( response.status ); return response.data; } catch ( error ) { console.log( error.message ); console.log( error.response.data ); } } async function exportDocument() { try { const config = getConfig( 'GET', exportApiEndpoint ); const response = await axios.get( exportApiEndpoint, config ); return response.data; } catch ( error ) { console.log( error.message ); console.log( error.response.data ); } } } )(); function getConfig( method, url, body ) { const CSTimestamp = Date.now(); return { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, method, url, CSTimestamp, body ) } }; } function generateSignature( secret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', secret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); } return hmac.digest( 'hex' ); } ``` 1. Update your credentials and the `documentId` values in the code snippet. 2. Execute the `node importExport.cjs` command. After a successful response with a status code `201` from the import request, you can open the editor with the same `documentId` as set in the snippet to see your imported content. Moreover, exported document content should be printed on the console.
### Troubleshooting * In case any operation during importing fails, e.g. fail while importing comments, all data of imported document will be revoked. * We suggest enabling [Insight Panel](#cs/latest/developer-resources/monitoring/insights-panel.html), where you will find detailed logs from the whole process including exact reasons for any failed requests. ### More info You will find more info in our [REST API documentation](https://help.cke-cs.com/api/docs#tag/Documents). source file: "cs/latest/guides/collaboration/document-storage.html" ## Document storage ### Overview The document storage feature allows you to permanently save the collaboration data in the CKEditor Cloud Services. When enabled, the documents are no longer [stored temporarily just for the duration of the document session lifetime](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data). Instead, the collaboration session data is saved permanently in the CKEditor Cloud Services server. Data saved in the document storage consists of the following: * **content** \- it is the full content of the edited document. It’s the equivalent of the results of calling [editor.getData()](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcore%5Feditor%5Feditor-Editor.html#function-getData). * **operations** \- it is every document update (document history) made during the editing session stored as an [object model](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fengine%5Fmodel%5Foperation%5Foperation-Operation.html). The operations are stored in the compressed state. * **version** \- the current version of the document. The version is determined based on the number of distinct operations performed. When a user connects to a previously existing document for which the collaboration session has already expired, a new collaboration session will be initialized using the content stored in the document storage. Your server is completely excluded from the responsibility of saving the document. You do not have to write any additional logic for getting the collaboration data, saving it in your database, and initializing the document with the previously saved data. These actions will be handled automatically by CKEditor Cloud Services. ### Prerequisites To use the document storage feature, an editor with its configuration needs to be uploaded to the CKEditor Cloud Services server. Cloud Services is using your editor to generate the correct output data before saving and to initialize the collaboration session with previously stored data. Thanks to this approach, your custom plugins that could generate some custom data will also work correctly. Two steps need to be taken before you can start using the feature: 1. Upload your editor bundle with the editor configuration to the CKEditor Cloud Services server. Refer to the [Editor bundle](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle) guide for more information. 2. Set the required [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion) property in the editor configuration. Refer to the [Editor configuration](#cs/latest/guides/collaboration/editor-bundle.html--editor-configuration) section of the editor bundle documentation for more details. ### Enabling the document storage feature Follow the steps below to set up the document storage feature. 1. Log in to the [Customer Portal](https://portal.ckeditor.com) and navigate to “Your products > Cloud Services”. 1. From the list of available subscriptions in the dashboard choose the subscription that you want to manage and click the “Manage” link. 1. Select the environment where you want to enable the document storage feature. Create one if needed with the “Create new environment” button. 1. Navigate to the “Feature configuration” tab and switch on the feature in the “Storage > Document storage” section. Remember that the document storage feature only works in the environment it was turned on for. If you use many environments, you need to enable the feature separately for each of these. ### Usage #### Initiating the collaboration session With the document storage enabled the sessions will be automatically created using the data stored in the CKEditor Collaboration Server databases. There are no additional actions required and initializing the editor with a `channelId` value will load the stored content. The process for initializing the collaboration session is as follows: 1. A user wants to open a document with a given `channelId` and the editor starts to initialize. 2. The CKEditor Collaboration Server checks if the content was already stored in the document storage. 3. Editor initializes with the data of the stored document. #### Saving the document After uploading the editor bundle and enabling the feature, the encrypted collaboration data will be saved automatically to the CKEditor Cloud Services database. There are several cases triggering the document save action: * an operation in the document made by any of the users (once every 5 minutes), * 1000 operations to the document made by the users, * the last user disconnects from the collaboration session, * a new document is created (either by the user or when a document is imported using the [REST API](#cs/latest/developer-resources/apis/overview.html)), * a collaboration session is flushed using the [REST API](#cs/latest/developer-resources/apis/overview.html). It is possible to trigger the document save action manually by using [REST API](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D~1save/put). You can also import the document synchronously to the storage through [REST API](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage/post) for as long as there is no active collaboration session with the same ID. #### Document storage webhooks You can receive information about the processing of your document in the document storage in the form of webhook events. Refer to the [document storage events](#cs/latest/developer-resources/webhooks/events.html--document-storage) section for more details. The document storage webhooks can be very useful when setting up the document storage. You can quickly verify that the documents are properly saved and there are no issues during this process. Some use cases could require having a copy of the content in your application. To sync the content between the CKEditor Collaboration server and your application, you can use the [storage.document.saved](#cs/latest/developer-resources/webhooks/events.html--document-saved) webhook to receive a signal that a newer version of the document is available. This can trigger a REST API request to [fetch](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage~1%7Bdocument%5Fid%7D/get) the content from the CKEditor Collaboration Server and then store it in your database. The recommended flow for keeping the documents in sync is as follows: 1. A user action or a REST API request triggers document storage saving. 2. The document content is saved in the CKEditor Collaboration Server databases. 3. The CKEditor Collaboration Server emits the `storage.document.saved` webhook, which includes the `channelId` property. 4. Once you receive the webhook, send a [GET /storage/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage~1%7Bdocument%5Fid%7D/get) request. 5. Store the content of the document in your database. #### Document management You can manage stored documents using the methods available from the [REST API](#cs/latest/developer-resources/apis/overview.html). The document storage methods available are: * [POST /storage](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage/post) – imports the content of a document to the storage. It can be either HTML content or a stringified JSON structure for documents created by a [multi-root editor](#ckeditor5/latest/examples/builds/multi-root-editor.html). In the latter case, the keys are the root names and the values are the HTML content for a particular root. * [GET /storage/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage~1%7Bdocument%5Fid%7D/get) – gets the content of a single document saved in the storage. It can be either HTML content or a stringified JSON structure for documents created by a [multi-root editor](#ckeditor5/latest/examples/builds/multi-root-editor.html). In the latter case, the keys are the root names and the values are the HTML content for a particular root. * [GET /storage](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage/get) – gets the list of the document IDs that are saved in the storage. * [DELETE /storage/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Storage/paths/~1storage~1%7Bdocument%5Fid%7D/delete) – deletes a single document from the storage. ### Example Check an [example of an application that uses the document storage feature in Node.js and Express.js](#cs/latest/examples/collaboration-examples/storage-feature-nodejs.html). Also, you can check the [CKEditor Cloud Services samples repository](https://github.com/ckeditor/cloud-services-samples), where you can find more examples. ### Debugging If needed, it is possible to store all collaboration data for debugging purposes. Thanks to this, our customer support team will be able to check the errors related to the document storage feature. Please enable this option only when you are asked to do so by our customer support team. source file: "cs/latest/guides/collaboration/editor-bundle.html" ## Editor bundle ### Overview An editor bundle is the source code of a pre-configured and ready-to-use CKEditor 5, which is minified and built as a single file. To use the [Server-side Editor API](#cs/latest/guides/collaboration/server-side-editor-api.html), [document storage](#cs/latest/guides/collaboration/document-storage.html), [import and export](#cs/latest/guides/collaboration/import-and-export.html), or [connection optimization](#cs/latest/guides/collaboration/connection-optimization.html) features, you need to upload an exact copy of the editor file from your application to the CKEditor Cloud Services server. CKEditor Cloud Services uses your editor bundle to convert operations in the collaboration session into data. Thanks to this, CKEditor Cloud Services can ensure data consistency between users and the stored data. Furthermore, if your editor bundle is using some custom plugins that can output some custom data, it will be then properly handled by CKEditor Cloud Services. Whenever you change something in your editor bundle or its configuration, you should update it on the CKEditor Cloud Services server, too. If the users use different versions of the editor than the one uploaded to the server, it may lead to data loss or even editor crashes. ### Building the editor bundle First, you need to create an editor bundle. You can follow our step-by-step [editor bundling guide](#cs/latest/examples/editor-bundle/advanced-editor-bundle-nodejs.html) or go with ready-to-use [editor bundle examples](#cs/latest/examples/editor-bundle/editor-bundle-nodejs.html). ### Uploading the editor bundle If you have already built your editor that meets the [requirements](#cs/latest/guides/collaboration/editor-bundle.html--building-the-editor-bundle), you can upload it to CKEditor Cloud Services together with the editor configuration. The editor upload process requires the use of the [POST /editors](https://help.cke-cs.com/api/v5/docs#tag/Editors/paths/~1editors/post) method from the Cloud Services REST API. Every request made to the CKEditor Cloud Services REST API needs to have additional headers: `X-CS-Timestamp` and `X-CS-Signature`. Refer to the [request signature](#cs/latest/developer-resources/security/request-signature.html) guide for more details. Three parameters are passed to the body of this method: * `bundle` – The code of your minified editor passed as a string. * `config` – This is the editor configuration that will be used by the editor in CKEditor Cloud Services. This should be the same config as used by the editor in your application, especially when it can alter the data output. * `test_data` – This is the real document content generated by the editor you upload to the CKEditor Cloud Services server. It helps to detect if any of your custom plugins modify the document content incorrectly or if an invalid configuration has been passed. This parameter is optional but it is highly recommended to use it. You should make sure that your test data has been created using all the custom plugins you have implemented. The body of the upload request should look like this: ``` { "bundle": "the content of editor bundle file", "config": { "cloudServices": { "bundleVersion": "unique_editor_bundle_version" }, "removePlugins": [ "myCustomAutoSavePlugin" ], ... other config options }, "test_data": "

example output from your editor

" } ``` The `bundleVersion` property inside the `config.cloudServices` object is required.
#### Editor configuration The `bundleVersion` property set during an upload acts as a unique build identifier. It tells CKEditor Cloud Services which editor should be used when a user starts the collaboration session. You need to make sure that the editor configuration in your application also has the [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion) property and its value matches the editor uploaded to the CKEditor Cloud Services server. An example editor configuration with set `bundleVersion` looks like this: ``` return CKEditorCS .create( document.querySelector( '.collaboration-demo__editable' ), { cloudServices: { tokenUrl: 'TOKEN_URL', uploadUrl: 'UPLOAD_URL', webSocketUrl: 'WEBSOCKET_URL', bundleVersion: 'editor-1.0.0' }, collaboration: { channelId: 'CHANNEL_ID' }, toolbar: [ // Toolbar items ], // Other config options } ); ``` #### Multi-root editor builds For [**multi-root** editor builds](#ckeditor5/latest/examples/builds/multi-root-editor.html), an upload request body requires an additional `is_multi_root_editor` field set to `true`. The field indicates that the uploaded editor is a multi-root build. Additionally, the `test_data` property is expected to be a stringified JSON structure where the keys are the root names and the values are the HTML content for particular roots. Moreover, if you want to use the roots’ attributes you must provide example values for each root in the editor config. ``` { "bundle": "the content of a multi-root editor bundle file", "config": { "cloudServices": { "bundleVersion": "unique_multiroot_editor_bundle_version" }, "removePlugins": [ "myCustomAutoSavePlugin" ], "rootsAttributes": { "main": { "order": 1 } }, ... other config options }, "test_data": "{\"header\":\"

Multi-root document header.

\",\"footer\":\"

Multi-root document footer.

\"}", "is_multi_root_editor": true } ```
#### Example Follow this example to learn how to properly upload the editor bundle to CKEditor Cloud Services. This example is using Node.js and `npm`, hence it is required to have these tools installed. It also assumes that you have already [built the editor bundle](#cs/latest/guides/collaboration/editor-bundle.html--building-the-editor-bundle). This example will continue using the files from the previous example. 1. In your terminal, use `cd` to get into the extracted folder and install the `axios` package with the `npm i axios` command. 2. Create a new `upload.js` file inside the extracted folder with the following content: ``` const crypto = require( 'crypto' ); const fs = require( 'fs' ); const path = require( 'path' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; const apiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/editors/`; const editorBundle = fs.readFileSync( path.resolve( './dist/editor.bundle.js' ) ); const body = { bundle: editorBundle.toString(), config: { cloudServices: { bundleVersion: 'editor-1.0.0' // Set a unique name for the uploaded bundle }, toolbar: [ // Toolbar items ], // Other config options } }; const CSTimestamp = Date.now(); const axiosConfig = { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, 'POST', apiEndpoint, CSTimestamp, body ) } }; axios.post( apiEndpoint, body, axiosConfig ) .then( response => { console.log ( response.status ); } ).catch( error => { console.log( error.message ); console.log( error.response.data ); } ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); } return hmac.digest( 'hex' ); } ``` Run the above code with the `node upload.js` command. You should see the `201` status code in your console. You can run `node upload.js` once again to make sure that the bundle is already uploaded. In this case, you will get a 409 error with message `Editor editor-1.0.0 already exists`. ### Updating the editor bundle As shown in the example above, it is not possible to overwrite an editor bundle on the CKEditor Cloud Services server. You may, however, encounter situations where you need to rebuild your editor, for example when you add a new plugin, update CKEditor 5 version or fix a bug in your custom plugin. You should follow these steps to start using a new editor bundle: 1. Rebuild your editor with the necessary changes to the configuration or plugins. 2. Upload the new editor bundle with the `bundleVersion` value being new and unique for your environment. 3. Update the [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion) in the editor configuration in your application to match the new value from step 2. 4. Wait for the active collaboration sessions [to be removed automatically](#cs/latest/guides/collaboration/data.html--temporary-and-permanent-data) or remove them manually using the [DELETE /collaborations/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete) from the CKEditor Cloud Services REST API. 5. Open the document in your new editor. A collaboration session initiated with certain `bundleVersion` can be opened only by editors with the same `bundleVersion` value. Because of this you can not connect to the previously edited documents with the new editor, without reinitializing such collaboration session (steps 4 and 5). source file: "cs/latest/guides/collaboration/import-and-export.html" ## Collaboration import and export ### Overview The process of saving the data of the document in an active collaboration session with multiple users can be challenging. A simple implementation using the [autosave plugin](#cs/latest/guides/collaboration/introduction.html--the-autosave-plugin) comes with some drawbacks. Another approach to initializing and saving collaboration data can solve most of them. The import and export feature, based on the [REST API](#cs/latest/developer-resources/apis/overview.html), improves the process of synchronizing the data of the document which is saved in your database. When using this feature, the responsibility for saving the current document data will be transferred from the users to your server. ### The Import and Export endpoints This feature utilizes two methods available from the REST API: * Import a document – Use this endpoint to set the initial content and create a new collaboration session. This endpoint will not work if a collaboration session with a given `documentId` already exists. * Export a document – Use this endpoint to fetch the content from an active collaboration session. In the response, you will get plain HTML or Markdown output from the editor (depending on the editor configuration), just like you would call `editor.getData()` on the client side. Exported document data may contain comments and/or suggestions markers if you are using corresponding plugins in your editor. To export a document without these markers use the [document import and export](#cs/latest/guides/collaboration/document-import-and-export.html) feature. Follow this guide to learn how to properly set up and use this feature. ### Prerequisites To use the import and export feature, an editor with its configuration needs to be uploaded to the CKEditor Cloud Services server. Cloud Services uses your editor to generate the correct output during the export and to validate the input during the import. Thanks to this approach, your custom plugins that could generate some custom data will also work correctly. Two steps need to be taken before you can start using the feature: 1. Upload your editor bundle with the editor configuration to the CKEditor Cloud Services server. Refer to the [Editor bundle](#cs/latest/guides/collaboration/editor-bundle.html--uploading-the-editor-bundle) guide for more information. 2. Set the required [bundleVersion](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html#member-bundleVersion) property in the editor configuration. Refer to the [Editor configuration](#cs/latest/guides/collaboration/editor-bundle.html--editor-configuration) section of the editor bundle documentation for more details. ### Usage #### Initiating the collaboration session Here is a recommended flow to successfully initialize an editing session using content stored in your database: 1. A user in your application that wants to open a document sends `channelId` (a new one or an already existing one) to your server. 2. Your server communicates with CKEditor Collaboration Server and uses the [GET /collaborations/{document\_id}/exists](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D~1exists/get) REST API endpoint to verify whether the session already exists. 3. Your server gets the document data from the database. This step should be skipped if the collaboration session already exists. 4. Your server initializes the collaboration session through the [POST /collaborations](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations/post) REST API endpoint. This step should be skipped if the collaboration session already exists. 5. Your server signals to the user that the session is ready to connect. 6. The editor initializes and connects the user to the collaboration session. #### Saving document data Depending on your use case, you may choose one or more of the following solutions to save the content from a collaboration session in your own database. ##### collaboration.document.exported webhook We recommend using the [collaboration.document.exported](#cs/latest/developer-resources/webhooks/events.html--collaboration-session-exported) webhook for saving document data. This webhook is emitted when a collaboration session is removed and already contains the content of the session. This ensures that the saved content includes all changes made by users during document editing and that you receive the latest version of the document. The recommended flow for using this webhook is as follows: 1. The collaboration session ends. You can either wait for it to end automatically 24 hours after the last user disconnects, or you can trigger it using the [DELETE /collaborations/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete) REST API endpoint. 2. CKEditor Collaboration Server emits the `collaboration.document.exported` webhook, which contains the content of the document and the `channelId` property. 3. Once you receive the data, store it in your database. ##### collaboration.document.update.exported webhook The above solution, which uses the `collaboration.document.exported` webhook, ensures that the latest possible version of the content is saved when the collaboration session ends. If you need frequent access to the current version of the content being edited in an active collaboration session, you can also use the [collaboration.document.update.exported](#cs/latest/developer-resources/webhooks/events.html--collaboration-session-update-exported) webhook. This webhook is periodically emitted during active editing sessions and after the last user disconnects from the collaboration session. Here is the recommended flow for using this webhook: 1. The webhook is triggered by: * adding an operation (once every 10 minutes) by the users, * adding 5000 operations by the users, or * the last user disconnecting from the collaboration session. 2. The `collaboration.document.update.exported` webhook, which contains the content of the document and the `channelId` property, is emitted from the CKEditor Collaboration Server. 3. After receiving the data, your server stores it in your database. ##### Export on demand If the triggers defined for the `collaboration.document.exported` and `collaboration.document.update.exported` webhooks do not match your use case, and you want to create custom triggers to obtain collaboration data, you could send a [GET /collaborations/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/get) request to CKEditor Collaboration Server to receive the content of the active collaboration session. The recommended flow for using export on demand looks as follows: 1. Some part of your application or a user action sends a request to your server with the `channelId` property. 2. Your server sends a [GET /collaborations/{document\_id}](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/get) request to CKEditor Collaboration Server to get the content of the collaboration session. 3. After receiving the data, your server stores it in your database. ### REST API Usage After successfully uploading your editor bundle to CKEditor Cloud Services you can start using the import and export feature. Every request made to Cloud Services REST API needs to have the following headers: * `X-CS-Timestamp` – the value of this header will be used for the signature generation and validation and it is very important to use a unique value for every request to make sure that the generated signatures will be unique as well. This value needs to be a number. Good example of a timestamp is the current Unix time. * `X-CS-Signature` – the value of this header is generated using a SHA-256 algorithm and is signed with the [API Secret](#cs/latest/developer-resources/security/api-secret.html). CKEditor Cloud Services are also generating signatures for every request and only accept the ones with a correct signature. Refer to the [Request signature guide](#cs/latest/developer-resources/security/request-signature.html) for more information. Check an [example of an application that uses this mechanism in Node.js and Express.js](#cs/latest/examples/collaboration-examples/import-export-nodejs.html) or follow this guide for more details on the feature’s usage. Also, you can check the [CKEditor Cloud Services samples repository](https://github.com/ckeditor/cloud-services-samples), where you can find more examples. #### Import example Importing a document means creating a new collaboration session using the previously saved document data. It is based on sending a request from your server to CKEditor Cloud Services using the [POST /collaborations](https://docs.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations/post) REST API method with the required data in the body: ``` { "document_id": "your_document_id", "bundle_version": "your_editor_id", "data": "

document_content

" } ``` At this point, CKEditor Cloud Services converts the document `data` sent in the request using the previously uploaded editor bundle with a set `bundle_version` and creates an active collaborative session for the `document_id`. After this operation, users can connect to the newly created collaboration session and continue working on the loaded document. This example is using node.js and `npm`. Make sure that you have these tools installed before you follow the next steps. This snippet assumes that you have already followed the steps from [Prerequisites](#cs/latest/guides/collaboration/import-and-export.html--prerequisites) section. 1. Run the following commands: ``` mkdir cs-import-example && cd cs-import-example npm init -y && npm i axios ``` 1. Create a new file named `import.js` in the `cs-import-example` folder with the following content: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; const apiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/collaborations/`; const body = { document_id: "document-1", // Set document_id of created collaboration session bundle_version: "bundleversion-1", // Use bundle_version from uploaded editor bundle data: "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

" }; const CSTimestamp = Date.now(); const config = { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, 'POST', apiEndpoint, CSTimestamp, body ) }, }; axios.post( apiEndpoint, body, config ) .then( response => { console.log ( response.status ); } ).catch( error => { console.log( error.message ); console.log( error.response.data ); } ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); }; return hmac.digest( 'hex' ); } ``` 1. Update your credentials, the `document_id` and `bundle_version` values in the code snippet. 2. Run the import with the `node import.js` command. After a successful response with a status code `204` you can open the editor with the same `document_id` as set in the snippet to see your imported content. You can also check the content using the export method.
#### Export example Exporting a document means getting the current data of the document from an active collaboration session. It is based on sending a request from your server to CKEditor Cloud Services using the [GET /collaborations/{document\_id}](https://docs.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/get) REST API method. At this point, using the previously loaded editor bundle, CKEditor Cloud Services converts an active collaboration session to the configured output form (HTML, Markdown, text etc.) which your server will receive in response to the request. Your server should save the received document data in the database. This example is using node.js and `npm`. Make sure that you have these tools installed before you follow the next steps. This snippet assumes that you have already followed the steps from the [Prerequisites](#cs/latest/guides/collaboration/import-and-export.html--prerequisites) section. 1. Run the following commands: ``` mkdir cs-export-example && cd cs-export-example npm init -y && npm i axios ``` 1. Create a new file named `export.js` in the `cs-export-example` folder with the following content: ``` const crypto = require( 'crypto' ); const axios = require( 'axios' ); // Update with your credentials and application endpoint const environmentId = 'txQ9sTfqmXUyWU5LmDbr'; const apiSecret = '4zZBCQoPfRZ7Rr7TEnGAuRsGgbfF58Eg0PA8xcLD2kvPhjGjy4VGgB8k0hXn'; const applicationEndpoint = 'https://33333.cke-cs.com'; const documentId = 'document-1'; // Set document_id of an active collaboration session const apiEndpoint = `${ applicationEndpoint }/api/v5/${ environmentId }/collaborations/${ documentId }`; const CSTimestamp = Date.now(); const config = { headers: { 'X-CS-Timestamp': CSTimestamp, 'X-CS-Signature': generateSignature( apiSecret, 'GET', apiEndpoint, CSTimestamp ) }, }; axios.get( apiEndpoint, config ) .then( response => { console.log ( response.data ); } ).catch( error => { console.log( error.message ); console.log( error.response.data ); } ); function generateSignature( apiSecret, method, uri, timestamp, body ) { const url = new URL( uri ); const path = url.pathname + url.search; const hmac = crypto.createHmac( 'SHA256', apiSecret ); hmac.update( `${ method.toUpperCase() }${ path }${ timestamp }` ); if ( body ) { hmac.update( Buffer.from( JSON.stringify( body ) ) ); }; return hmac.digest( 'hex' ); } ``` 1. Update your credentials and the `documentId` value in the code snippet. 2. Run the export with the `node export.js` command. You should see the content of an active collaboration session in your console. source file: "cs/latest/guides/collaboration/introduction.html" ## Introduction ### Saving the collaboration data This guide covers four separate methods of reading the content of a collaboration session to save it to some permanent storage using: * **[The Cloud Services REST API](#cs/latest/guides/collaboration/introduction.html--export-using-the-cloud-services-rest-api)** – a server-side solution, where your server can send a request to Cloud Services Collaboration Server to receive the content of the edited document to store it in your database. * **[Saving data with Webhooks](#cs/latest/guides/collaboration/introduction.html--saving-data-with-webhooks)** – a server-side solution, where your server receives requests (webhooks) with content of the documents when the collaboration sessions end. * **[Document Storage](#cs/latest/guides/collaboration/introduction.html--document-storage)** – a Cloud Services solution, where the document data is permanently saved in the CKEditor Cloud Services server. * **[The Autosave plugin](#cs/latest/guides/collaboration/introduction.html--the-autosave-plugin)** – a client-side solution, through which the users periodically send the content of the collaboration session to your server. #### Export using the Cloud Services REST API This mechanism, based on the [REST API](#cs/latest/developer-resources/apis/overview.html), was introduced as an alternative to the [autosave plugin](#ckeditor5/latest/features/autosave.html). It improves and simplifies the process of synchronizing the data of the document which is saved in your database. Using this feature, the responsibility for saving the current document data will be transferred from collaborating users to your server. As a result, it is your server that gets the current content of the document from CKEditor Cloud Services. Please compare the diagram showing the data workflow to notice the differences: The number of requests is largely reduced – there is just one connection present between the CKEditor Cloud Services server and the client’s server. The whole responsibility of serving and storing the content is handled by server-server connection. The collaborator’s only role is content editing. ##### The benefits of using the Cloud Services REST API * The user is sure to use the latest version of the document. There is no risk of [race condition](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html--the-clouddocumentversion-property), data inconsistency or other problems related to document synchronization. * Reduced number of requests to your server. Collaborating users no longer need to send the document data in time intervals. * Increased communication security. The synchronization process takes place only between your server and the CKEditor Cloud Services server using an encrypted SSL connection and [request signatures](#cs/latest/developer-resources/security/request-signature.html). * The process of saving the document data is independent of the network conditions in which the users work. * The content in the collaboration session can be initialized by the client’s server. * One export operation means only one document content which allows you to save some storage and get rid of minor changes to the document versions. There are two types of export: * [Document export](#cs/latest/guides/collaboration/document-import-and-export.html) – exports current collaboration session content or document storage content (if present and no current collaboration session exist), and all document-related data like comments and suggestions. Check out the [guide](#cs/latest/guides/collaboration/document-import-and-export.html) for the implementation details. * [Collaboration export](#cs/latest/guides/collaboration/import-and-export.html) – exports only current collaboration session content. Check out the [guide](#cs/latest/guides/collaboration/import-and-export.html) for the implementation details. #### Saving data with webhooks This approach is similar to REST API usage, but the main difference is the direction of the requests. With REST API export endpoints you send a request to the Cloud Services server and the server responds with the content of the edited documents. When using webhooks, your server (or serverless functions) waits for the requests from the Cloud Services server instead. To use the feature, you should enable [webhooks](#cs/latest/developer-resources/webhooks/overview.html) in the [Customer Portal](https://portal.ckeditor.com) or in the [Cloud Services Management Panel](#cs/latest/onpremises/cs-onpremises/management.html), provide a URL to your server and enable the [collaboration.document.exported](#cs/latest/developer-resources/webhooks/events.html--collaboration-session-exported) event. When the collaboration session ends, the request with the document content will be sent to the provided URL. Refer to the webhooks section in the documenation for more details. Also, check [other webhook events related to collaboration](#cs/latest/developer-resources/webhooks/events.html--collaboration) that can be useful when using webhooks to save document data. The collaboration session ends 24 hours after the last user disconnects from it. If your application needs to access the document content before the collaboration session expires it is possible to manually remove the collaboration session using [a flush method](https://help.cke-cs.com/api/v5/docs#tag/Collaboration/paths/~1collaborations~1%7Bdocument%5Fid%7D/delete) from Cloud Services REST API. This will also trigger a `collaboration.document.exported` webhook. You could also use the [collaboration.user.disconnected](#cs/latest/developer-resources/webhooks/events.html--user-disconnected) webhook event to check if there are no users connected and then trigger the flush to get the data in the `collaboration.document.exported` webhook. Please, keep in mind that this could impact the UX, when users refresh a page or have some network issues. It is recommended to use debounce or timeout mechanism that will cover brief disconnects from the collaboration sessions. #### Document storage Cloud Services also offer the [document storage](#cs/latest/guides/collaboration/document-storage.html) feature that allows you to permanently save the document data in CKEditor Cloud Services. In this case, the client-server handling used to store the document data is replaced with Cloud Services server. This is especially convenient for clients that value speed and security. When a user connects to a previously existing document for which a collaboration session has already expired, the document is automatically loaded into a new collaboration session. ##### Benefits of the document storage feature * Reduced number of requests to your server. Collaborating users no longer need to send the document data in time intervals. * Your server is completely excluded from the responsibility of saving the document. * The process of saving the document data is independent of the users’ or your server’s network conditions. * No need to check whether a collaborative document session currently exists. * No need to load the current content of the document in case the collaboration session has expired. * No [race condition problem](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html--the-clouddocumentversion-property) when saving the document data. * Documents are saved in an encrypted form. * It is possible to get or delete documents from the storage or get the documents list via the REST API. Check out the [document storage guide](#cs/latest/guides/collaboration/document-storage.html) for implementation details. #### The autosave plugin While using the [autosave plugin](#ckeditor5/latest/features/autosave.html), user data is periodically sent to the client’s server, where it can be handled, validated and stored in the database. See the diagram below for the details of the process. This method, although the simplest to implement, has its drawbacks: * There is a race condition issue when many users are working on the same document and you need to ensure that you save the most recent version of the document. * The data is coming from an untrusted source (the end-user) and may be manipulated. * When opening previously edited documents, the user needs to fetch the content from the database to use it as the initial data. * It increases the number of requests and the traffic to your server. * It may be slow, especially when a large document is edited by a user with a poor internet connection. ### Ways of initializing the collaboration session with data The users will not always start from an empty editor and finish the document in one collaboration session. There is hence the need to initialize the editor with the previously saved versions of content or with some generated templates. Similar to getting the collaboration data, there are few ways to start a new collaboration session with your content: * Use the [initialData](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcore%5Feditor%5Feditorconfig-EditorConfig.html#member-initialData) property in the editor configuration – the content passed to this property will be used when the user opens the editor. It will not overwrite the data if the collaboration session already exists. * Use [document import](#cs/latest/guides/collaboration/document-import-and-export.html) from the Cloud Services REST API – your server can initialize the content in the collaboration session with all comments and suggestions with a single API call, and then the users will be able to connect to this collaboration session. * Use [collaboration import](#cs/latest/guides/collaboration/import-and-export.html) from the Cloud Services REST API – your server can initialize the content in the collaboration session ( without comments and suggestions) with a single API call, and then the users will be able to connect to this collaboration session. * Use [document storage](#cs/latest/guides/collaboration/document-storage.html) – if you enabled this feature, then opening previously edited documents will be automatically initialized with the saved content. Refer to the [import and export](#cs/latest/guides/collaboration/import-and-export.html) and [document storage](#cs/latest/guides/collaboration/document-storage.html) guides for implementation details. source file: "cs/latest/guides/collaboration/migrating-to-rtc.html" ## Migrating to Real-time Collaboration ### Overview This article aims to explain the prerequisites and process of migrating Non Real-time Collaboration Features to the Real-time version. Thanks to this, your documents will be able to be edited simultaneously by many users. Your application will also be excluded from the responsibility of processing and synchronizing the data. #### Differences between Real-time and non Real-time Collaboration In non Real-time Collaboration you have to implement adapters for each object that would process the data from the editor. A server application has to be implemented as well to store and synchronize the data: In [Real-time Collaboration](#ckeditor5/latest/features/collaboration/collaboration.html) all the logic required for synchronising and saving the data is handled by the CKEditor5 Real-time Collaborative plugins and the CKEditor Cloud Services. ### Prerequisites In order to take full advantage of Real-time Collaboration Features you will need: 1. Active CKEditor Cloud Services [subscription](#cs/latest/guides/collaboration/quick-start.html--subscribe-to-the-collaboration-service). 2. A Real-time Collaboration editor build integrated with [CKEditor Cloud Services](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html). 3. A [token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) prepared. It is required to authenticate users connected to the Real-time Collaborative Editing Sessions. ### Migrating your data to the Real-time Collaboration The migration process can be summarized in these steps: 1. For each separate editor build you were using prepare a separate editor bundle with RTC features. 2. Get data for each [document](#cs/latest/guides/basic-concepts/data-model.html--document) and each [user](#ckeditor5/latest/features/collaboration/real-time-collaboration/users-in-real-time-collaboration.html) you want to migrate to the Real-time Collaboration from your server 3. Map the data according to the CKEditor Cloud Services requirements and upload it using the [REST API](#cs/latest/developer-resources/apis/overview.html). Please note that all requests to the CKEditor Cloud Services REST API require [authentication](#cs/latest/developer-resources/apis/authentication.html). #### Uploading editor bundle To take full advantage of the Real-time Collaboration an [editor bundle](#cs/latest/guides/basic-concepts/editor-bundle.html) has to be uploaded to the server. For each editor build you were using in the asynchronous collaboration a respective bundle would have to be created. Since in Real-time Collaboration your server is excluded from the responsibility of synchronizing the data, the build does not have to include adapters for each resource (comments, track changes, etc.). To ensure data consistency, the bundled editor should include **every other plugin** that was used in the non-RTC editor. You can learn more about preparing and uploading an editor bundle in our dedicated [guideline](#cs/latest/guides/collaboration/editor-bundle.html) #### Migrating user data The real-time collaborative editing plugin will define user data and permissions automatically based on [your token data](#cs/latest/developer-resources/security/token-endpoint.html--user). If you want to migrate the user data manually your server would also have to store the user data and it would also have to be prepared at this step. For each user you want to migrate call the [REST API endpoint POST /users](https://help.cke-cs.com/api/v5/docs#tag/Users/paths/~1users/post) #### Migrating document data To be able to collaborate in real-time on a given document its data would have to be migrated to the CKEditor Cloud Services. In order to do so, you need to get the data from your server application for each resource present on the document and map each field according to the requirements specified in the [REST API endpoint POST /documents](https://help.cke-cs.com/api/v5/docs#tag/Documents/paths/~1documents/post) and call it. After a successful response from the server, you can start collaborating on the document in Real-time and let the CKEditor Cloud Services handle the rest. For Real-time Collaboration to work properly some fields not present in the non-RTC collaboration are required: 1. `suggestion.author_id` – Each suggestion in Real-time Collaboration should be linked with a user that requested it. In Real-time Collaboration this value is set automatically. If you don’t save information about who posted the suggestion on your server, you can either set some anonymous user as the suggestion author or create a user using Users REST API and then assign it as the author of every suggestion from the non-RTC collaboration. 2. `content.version` – Each document has its version set based on the operations performed. This value is required to import revisions to the Real-time Collaboration. If you want to migrate document with revisions you can set this value as the highest `toVersion` value from migrated revisions. 3. `comment.type` – This field is used by the CKEditor Cloud Services to determine whether a given comment is placed under the suggestion or another comment. The type for a regular comment is `1` and for the suggestion comments it’s `2` If you don’t store such information you can determine it by trying to find a matching suggestion by comparing its ID with the ID of a given comment thread. If for a given comment thread a matching suggestion would be found, you can set `type = 2` for each comment under that comment thread. Otherwise, it should be set to `1`. A simplified example can look as follows: ``` function getCommentThreadsData( commentThreadsForDocument, suggestionsForDocument ) { const threads = []; const comments = []; commentThreadsForDocument.forEach( commentThread => { const hasMatchingSuggestion = suggestionsForDocument.some( suggestion => suggestion.id === commentThread.id ); threads.push( { // map your comment thread here } ); comments.push( ...commentThread.comments.map( comment => ( { // map other comment fields here // set comment type depending if a comment thread it belongs to has a matching suggestion type: hasMatchingSuggestion ? 2 : 1 } ) ) ); } ); return { threads, comments } } ``` ### Example Check an [example of an application that migrates documents to Real-time Collaboration in Node.js and Express.js](#cs/latest/examples/collaboration-examples/migrating-document-to-rtc.html). ### Next steps * Learn about how the [CKEditor Cloud Services stores and manages data](#cs/latest/guides/collaboration/data.html). * Learn about [RTC features](#cs/latest/guides/collaboration/introduction.html). * Learn about [system security and data safety](#cs/latest/guides/system-security.html). source file: "cs/latest/guides/collaboration/quick-start.html" ## Quick start The aim of this article is to get you up and running with [CKEditor 5 Collaboration Features](https://ckeditor.com/collaboration/). Follow the steps below: * Please [contact us](https://ckeditor.com/contact/) to purchase the Real-time collaboration features license. * Generate your access credentials in the [Customer Portal](https://portal.ckeditor.com/). * Write a script that generates one-time tokens for authorizing end users of your application in CKEditor Cloud Services (using access credentials created earlier). * Create a [CKEditor 5 build with support for collaboration](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html) and configure it. All steps are explained in detail below. ### Subscribe to the Collaboration service Sign up for the [Collaboration service](https://portal.ckeditor.com/checkout?plan=free). After signing up, you will receive access to the customer dashboard (Customer Portal). ### Log in to the Customer Portal Log in to the [Customer Portal](https://portal.ckeditor.com) and navigate to “Cloud environments”. ### Create token endpoint You now need to create a [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) in your application. The role of this endpoint is to securely authorize end users of your application to use CKEditor Cloud Services only if they should have access to the content or action they are requesting. #### Development token endpoint If you are just starting, you may use the development token endpoint URL which is available out of the box and requires no coding on your side. The URL of the development token endpoint can be obtained easily in two simple steps: 1. From the list of environments select the one that you want to manage. To create a new environment, follow [instructions](#cs/latest/developer-resources/environments/environments-management.html). 1. The development token URL is accessible at the bottom of this section: ##### Simulating random users and roles The development token URL will generate random user data by default. As mentioned above, the user data will be returned from the pool of 10 predefined users. If you would like to specify your user details or set different roles and permissions, you may pass them in the query string using: * `sub` (Optional) – The unique ID of the user in your system. * `user.name` (Optional) – Full name. * `user.email` (Optional) – Email address. * `user.avatar` (Optional) – The URL to an avatar. * `data` (Optional) – The custom data can be used to store any data. * `role` (Optional) – User [role](#cs/latest/developer-resources/security/roles.html--roles) set in the token payload. The default role is `writer`. * `additionalPermissions` (Optional) – Additional [permissions](#cs/latest/developer-resources/security/roles.html--permissions) that will be added in the token payload. When adding multiple permissions, separate them with `,` character. So, if your token URL is `https://17717-dev.cke-cs.com/token/dev/XXX` then you may connect to CKEditor Cloud Services as the user _Jane Doe_ with the ID _13_ using the customized development token URL: `https://17717-dev.cke-cs.com/token/dev/XXX?user.name=Jane%20Doe&sub=13` ##### Extending the default user limit There might be a need to extend the default limit of 10 development users. In order to do this, add a limit query parameter to your dev token URL. For example: `https://17717-dev.cke-cs.com/token/dev/XXX?limit=40` Using the above URL you will obtain a token for a user from a pool of 40 predefined users. Bear in mind that after the 10th token, you will start paying for development users. The maximum allowed value for the limit parameter is 50\. Therefore, the maximum number of paid users will be equal to 40 with the usage of our predefined users. Please remember that using your own generated user IDs may result in an unpredictable number of paid users. #### Writing your own token endpoint To write your own [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html), you need to create access credentials for the selected environment by going to the “Access credentials” tab and clicking the “Create a new access key” button. Read more in the [Creating access credentials](#cs/latest/developer-resources/security/access-key.html--creating-access-credentials) section of the Environments management guide. ### Cloud region Please note that Cloud Services can [reside in either US or EU region or in both](#cs/latest/guides/saas-vs-on-premises.html--cloud-region). The region is set per subscription and cannot be changed for existing environments by the user. For Custom plan with multi-region it is possible to choose the region during environment creation. This topic is addressed in more detail in the [Environment management](#cs/latest/developer-resources/environments/environments-management.html--cloud-region) guide. ### Next steps [Learn how to enable collaboration features in CKEditor 5](#ckeditor5/latest/features/collaboration/real-time-collaboration/real-time-collaboration-integration.html). source file: "cs/latest/guides/collaboration/server-side-editor-api.html" ## Server-side Editor API The Server-side Editor API enables deep and complex integration of your application with all document data, enabling you to manipulate content and manage collaborative data such as suggestions, comments, and revision history, and much more, directly from your server-side code. The remote script REST API endpoint allows you to execute any JavaScript code that uses the CKEditor 5 API, that could be executed by a browser, but without the need to open the editor by a human user. Instead, the script is executed on the Cloud Services server allowing for updating and interacting with active collaboration sessions remotely. ### Why use server-side editor API? There are many scenarios where server-side content processing is essential: * **Automation**: Run content processing tasks as part of your backend workflows. * **Scalability**: Process multiple documents simultaneously without client-side limitations. * **Security**: Process sensitive content in a controlled environment without exposing it to client-side manipulation. * **Performance**: Handle large-scale content operations without impacting the user’s browser. * **Consistency**: Ensure uniform content changes across multiple documents. * **Integration**: Connect with other server-side systems and databases directly. ### Common use cases * **Deep integration**: Build custom features that can manage document content and related document data straight from your application UI, without a need to open the editor. * **Content migration**: Restructure and update references across multiple documents, perfect for website redesigns or content reorganization. * **Shared content blocks**: Automatically update reusable content (like headers, footers, or common sections) across all documents that use it. * **Automated review systems**: Build systems that automatically review and suggest content changes, like grammar checks or style improvements. * **AI-powered editing**: Make automated suggestions while users are actively editing, helping improve content quality. * **Automated publishing**: Prepare and process content for publication, including formatting, metadata updates, and resolving comments. ### Quick example Here’s a basic example of how to use the Server-side Editor API: **Endpoint:** ``` POST /collaborations/{document_id}/evaluate-script ``` **Request body:** ``` { "script": "const content = editor.getData(); return { content: content, wordCount: content.split(' ').length };", } ``` **Response:** ``` { "data": { "content": "

Document content here...

", "wordCount": 15 } } ```
### Next steps * [Server-side Editor API reference](#cs/latest/developer-resources/server-side-editor-api/editor-scripts.html): learn more details about the requests and response structure of editor scripts for server-side content manipulation. source file: "cs/latest/guides/export-to-pdf/overview.html" ## Export to PDF - overview CKEditor Cloud Services offer a fast and highly scalable service enabling the user to export documents to PDF document. The feature is available as a service, making it possible to send an HTML string straight into the Cloud Services server for more advanced use or as convenient WYSIWYG editor plugins for the ease of use in less demanding cases. ### Pagination feature The complementary premium [pagination feature for CKEditor 5](#ckeditor5/latest/features/pagination/pagination.html) allows you to see where page breaks would be after the document is exported to PDF. Thanks to the live preview, the user is able to fine-tune the structure of the output document when editing it. In addition to this, the pagination feature shows you the page count and allows to easily navigate between the document pages. ### Integration with merge fields (content placeholders) [Merge fields](#ckeditor5/latest/features/merge-fields.html) are visually distinct placeholder elements you can put into the content to mark places where real values should be inserted. It is perfect for creating document templates and other kinds of personalized, serialized content. This allows for automation and creating batch output of personalized PDF files. Learn how to configure it in the [configuration section of the merge fields guide](#ckeditor5/latest/features/merge-fields.html--using-callbacks-to-define-values). ### How the Export to PDF feature works The HTML to PDF converter provides an API for converting HTML documents to PDF files. The service generates a file and returns it to the user so they can save it in the `.pdf` format on their disk. To integrate the feature into your environment, first check the [Quick start](#cs/latest/guides/export-to-pdf/quick-start.html) guide to start using it. You can also test the feature using [the export to PDF demo page](https://pdf-converter.cke-cs.com/v2/convert/demo). ### The HTML to PDF converter request The following properties are available while using the converter service: * [html](#cs/latest/guides/export-to-pdf/overview.html--html) * [css](#cs/latest/guides/export-to-pdf/overview.html--css) * [config](#cs/latest/guides/export-to-pdf/overview.html--configuration) If you want to use this API directly, you need to provide proper CSS code manually. To achieve this, you should pass a string that is a concatenation of the default editor styles (refer to the [CKEditor 5’s content styling](#ckeditor5/latest/getting-started/setup/css.html--styling-the-published-content) guide) and, optionally, your custom styles to the `css` property. With the default editor configuration, you do not need to specify the `config` property. #### HTML Any `HTML` compliant content can be sent to the converter. * All HTML documents use the `` declaration to generate PDF files. * The recommended way to generate a PDF document is to send only the HTML content. However, if your document requires a full HTML structure to render properly, you can use it. #### CSS This option is used to send custom CSS styles. #### Configuration The `config` section defines the details of your PDF conversion request. Options such as margins, page size, and orientation are available to control the visual arrangement of the document. The implementation of mirror margins facilitates book-like layouts. You can define headers and footers for the output PDF. The converter supports distinct header and footer variations (first page, odd pages, even pages, and default) to allow for differentiated content across pages. Beyond content and layout, the configuration allows for control over other PDF attributes. You can set PDF metadata to enhance document discoverability for search and cataloging purposes. Password protection can be applied to restrict file access. You can also include digital signatures to confirm the origin and integrity of the PDF. Refer to the [converter documentation](https://pdf-converter.cke-cs.com/v2/convert/docs#section/Export-to-PDF-%28v2%29/Configuration) for details on the syntax and usage of all the available options. #### Images Images can be either inserted from a URL or Base64-encoded, using the `src` attribute of the `` tag. #### Web Fonts To generate PDF documents, you can use web fonts like in a regular browser. You need to specify the URL to the font file or a font file converted to the Base64 format. Refer to the [converter documentation](https://pdf-converter.cke-cs.com/v2/convert/docs#section/Web-Fonts) for details and examples. ### The Export to PDF editor plugin The [Export to PDF CKEditor plugin](#ckeditor5/latest/features/converters/export-pdf.html) allows you to easily print the WYSIWYG editor content to a PDF file. When enabled, this feature sends the content of your editor together with the styles that are used to display it to the CKEditor Cloud Services HTML to PDF converter service. The service then generates a PDF document that the user can download. Thanks to this plugin, it takes exactly one button click to get a PDF file with content formatted the same way as displayed in the CKEditor WYSIWYG editor. An easy solution for integrations, that does not demand editor data to be processed further once it is edited. ### Stored data Export to PDF is a service that generates PDF files based on the data provided by the customer (HTML, CSS). To perform the conversion, it requires the data sent from the editor or a database, but the service itself is fully stateless. The data (especially your sensitive data: HTML, CSS, comments) is used only during the processing. However, we store some information needed for billing and maintaining our system. We try to reduce stored data to a required minimum and at this moment we store such information as the number and duration of exports, the final document size, the number of unique exports, statistical data (used features, IP addresses, editor versions). All logs related to processing requests are saved only in case of unexpected situations and are stored for 90 days. The data in logs includes an error message, stack trace, and information about the request. source file: "cs/latest/guides/export-to-pdf/quick-start.html" ## Export to PDF - quick start The aim of this article is to get you up and running with the Export to PDF service. Follow these steps: * Subscribe to Export to PDF on the [Premium Features Free Trial website](https://portal.ckeditor.com/checkout?plan=free). * Generate your access credentials in the [Customer Portal](https://portal.ckeditor.com/). * Configure CKEditor. All the steps are explained in details below. ### Subscribe to CKEditor Cloud Services Create an account in CKEditor Cloud Services by signing up to Export to PDF. After signing up, you will receive access to the customer dashboard (Customer Portal). ### Log in to the Customer Portal Log in to the [Customer Portal](https://portal.ckeditor.com) and navigate to “Your products > Cloud Services”. From the list of available subscriptions in the dashboard choose the Export to PDF subscription that you want to manage and press the “Manage” link. ### Using the service with REST API The Export to PDF service may be used to convert an HTML string into a PDF file. In order to use it, authorization is needed in the form of an authorization token. The authorization token need to be passed as a value of the `Authorization` header. ``` { 'Authorization': 'Your generated authorization token' } ``` #### Generating the authorization token The authorization token can be generated in two ways: 1. As a value returned from the [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) URL. 2. Locally created token. #### Creating access credentials From the list of environments select the one that you want to manage. Go to the “Access credentials” tab and click the “Create a new access key” button. The modal will show up and will prompt for a name for the new access key. Provide the name and click the “Save” button. The newly created access key will be present on the list of access keys. For your safety, the access key value will disappear in a few seconds. You will be able to display it again by clicking the “Show” button. Make sure to keep the access credentials in a safe place. Read more about dealing with the credentials in the [Managing access credentials](#cs/latest/developer-resources/security/access-key.html--managing-the-access-credentials) section of the Environments management guide. #### Example request Tokens for the HTML to PDF converter service require passing a payload with the `aud` parameter and a valid `iat`: * `aud` – The environment ID. * `iat` – Issued at. The `accessKey` value needs to be replaced with your own access key generated before. The following example presents a request that contains the required `Authorization` header for the HTML to PDF converter: ``` const fs = require( 'fs' ); const jwt = require( 'jsonwebtoken' ); const axios = require( 'axios' ); const accessKey = 'iGC6md5EcTG0DDZqEf4m4eytr679z0Ch2gIQuraBPzyWEtTbcpQAqAhA9LxpkDtpn'; const environmentId = 'DO3zekDU3LiJX8GOL2xt'; const token = jwt.sign( { aud: environmentId, iat: Math.floor( Date.now() / 1000 ) }, accessKey, { algorithm: 'HS256' } ); const data = { html: '

I am a teapot

', css: 'p { color: red; }', config: { document: { margins: { top: '2cm' } } } }; const config = { headers: { 'Authorization': token }, responseType: 'arraybuffer', }; axios.post( 'https://pdf-converter.cke-cs.com/v2/convert/html-pdf', data, config ) .then( response => { fs.writeFileSync('./file.pdf', response.data, 'binary') } ).catch( error => { console.log( error ); } ); ``` Please refer to the [converter documentation](https://pdf-converter.cke-cs.com/v2/convert/docs) to start using the REST API service.
### Using the Export to PDF feature as a plugin In order to use the export to PDF feature in your CKEditor 5 build, a token URL is needed for authorization. #### Creating token endpoint Before you can use the feature, you first need to create a [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html) in your application. The role of this endpoint is to securely authorize end users of your application to use CKEditor Cloud Services only if they should have access to the content or action they are requesting. ##### Development token endpoint (development only) If you are just starting, you may use the development token endpoint URL which is available out of the box and requires no coding on your side. The URL of the development token endpoint can be obtained easily in three simple steps: 1. To work with CKEditor premium features like real-time collaboration, exports and import you need to create an environment. Environments allow you to create access credentials, manage webhooks, configure features and connect to CKEditor Cloud Services. You may have more than one to serve separate instances or integrations. From the list the environments choose the one that you want to manage. 1. The development token URL will show up in the “Development token URL” section: ##### Writing your own token endpoint (production) To write your own [security token endpoint](#cs/latest/developer-resources/security/token-endpoint.html), you need to create access credentials for the selected environment by going to the “Access credentials” tab and clicking the “Create a new access key” button. Read more in the [Creating access credentials](#cs/latest/developer-resources/security/access-key.html--creating-access-credentials) section of the Environments management guide. #### Integrating the Export to PDF plugin with the application ##### Enabling the plugin in CKEditor 5 To install the plugin into your WYSIWYG editor, use the [online builder](https://ckeditor.com/ckeditor-5/online-builder/) to generate a custom CKEditor 5 build with the plugin enabled. Alternatively, refer to the [installation guide](#ckeditor5/latest/features/converters/export-pdf.html--installation) in the plugin documentation to do it on your own. Configure the plugin using the token generated earlier, pasting the link in the configuration (see [CloudServicesConfig](https://ckeditor.com/docs/ckeditor5/latest/api/module%5Fcloud-services%5Fcloudservicesconfig-CloudServicesConfig.html)): ``` ClassicEditor .create( document.querySelector( '#editor' ), { exportPdf: { tokenUrl: 'https://example.com/cs-token-endpoint', } } ) .then( ... ) .catch( ... ); ``` This is all. At this point, the export will be automatically enabled in your application. Refer to the [CKEditor 5 guide](#ckeditor5/latest/features/converters/export-pdf.html--installation) for further details on adding the Export to PDF feature to your WYSIWYG editor! ##### Enabling the plugin in CKEditor 4 You can use the [Online builder for CKEditor 4](https://ckeditor.com/cke4/builder) to integrate the Export to PDF plugin into your build. There is just one thing you have to do to activate plugin - set a `exportPdf_tokenUrl` configuration option: ``` CKEDITOR.replace( 'editor', { exportPdf_tokenUrl: 'https://example.com/cs-token-endpoint' } ) ``` Refer to the [Export to PDF configuration guide](https://ckeditor.com/docs/ckeditor4/latest/features/exporttopdf.html#configuration) for step-by-step integration walk-through. #### Using additional HTTP headers If fetching some resources (for example, images) used in a generated PDF requires passing an additional authorization factor in the form of additional HTTP headers, you can define it in PDF `config.extra_http_headers`: ``` const data = { html: '

I am a teapot

', css: 'p { color: red; }', config: { extra_http_headers: { 'https://secured-example-website.com': { authorization: 'Bearer RDp0NqyePooNWFIWvHtbKrKKHLXfmLfZcv3PRpCyJI90uwi3pKvumKl2vymCxoGFw6Vx' } } } }; axios.post( 'https://pdf-converter.cke-cs.com/v2/convert/html-pdf', data, config ) .then( response => { fs.writeFileSync('./file.pdf', response.data, 'binary') } ).catch( error => { console.log( error ); } ); ```
source file: "cs/latest/guides/export-to-word/collaboration-features.html" ## Supported collaboration features The Export to Word converter supports Word’s collaboration features, namely the comments and track changes. The comments feature lets you add a notice about a selected part of the content, whereas track changes allow for proposing a change inside the content, like deletion, insertion, or replacement of a selected part of the content. Both features are also implemented by CKEditor 5, so make sure to read [CKEditor 5 comments](#ckeditor5/latest/features/collaboration/comments/comments.html) and [CKEditor 5 track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html) guides to learn more. Note that despite that Export to Word requires HTML in a format compatible with CKEditor 5 collaboration features, the converter alone is technology-agnostic. It can be used with custom integrations on your end – as long as it supports the same content format. ### Comments Comments are an advanced feature of Word that is supported by the Export to Word feature with full compatibility with [CKEditor 5 comments](#ckeditor5/latest/features/collaboration/comments/comments.html). Comments allow users to add notes to the document without changing the content itself. Comments data is supplied to the converter via the `config.collaboration_features.comment_threads` configuration option. Comments metadata consists of an array of comment threads, which contain the following properties: * `thread_id`: A string ID of the thread (**required**). * `is_resolved`: A boolean that determines whether the thread is resolved (**defaults to false**). * `comments`: An array of comments belonging to the thread. They contain the following properties: * `content`: HTML content of the comment (**required**). * `author`: A comment author’s name (**required**). * `created_at`: Date and time describing when the comment was created in ISO 8601 format (**optional**). An example of converting HTML content with comments to a Word document: ```

Hello world!

``` ``` { "config": { "collaboration_features": { "comment_threads": [ { "thread_Id": "comment-thread-1", "resolved": false, "comments": [ { { "author": "User 1", "content": "

This is an example comment.

", "created_at": "2024-05-12T10:00:00.000Z", }, { "author": "User 2", "content": "

This is another example comment.

", "created_at": "2024-05-12T10:30:00.000Z", }, } ] } ] } } } ``` Export to Word converter supports converting [paragraphs](#cs/latest/guides/export-to-word/content-formatting.html--paragraphs), [inline HTML elements](#cs/latest/guides/export-to-word/content-formatting.html) and [hyperlinks](#cs/latest/guides/export-to-word/content-formatting.html--hyperlinks) as content of comments.
### Track changes Track changes is another advanced feature that is also compatible with the [CKEditor 5 track changes](#ckeditor5/latest/features/collaboration/track-changes/track-changes.html). To start tracking changes in Word, select the Review tab from the ribbon and then enable the Track Changes option. Now any changes applied to the document will be tracked. Track changes data is supplied to the converter via the `config.collaboration_features.suggestions`. Track changes metadata consist of an array of suggestions with the following properties: * `id`: A string ID of the suggestion (**required**). * `author`: A suggestion author’s name (**required**). * `created_at`: Date and time describing when the suggestion was created in ISO 8601 format (**optional**). Example of converting HTML content with suggestions to Word document: ```

Insertion

Deletion

``` ``` { "config": { "collaboration_features": { "suggestions": [ { "id": "0", "author": "User 1", "created_at": "2024-05-12T10:00:00.000Z", }, { "id": "1", "author": "User 1", "created_at": "2024-05-12T10:30:00.000Z", } ] } } } ```
#### Timezone The word track changes feature does not support dates with a specified time zone and all suggestions are treated as local dates and times. You can use the `config.timezone` option to offset such dates to a specified time zone. Consider the conversion of the following document: ``` { "html": "Suggestion", "config": { "collaboration_features": { "suggestions": [ { "id": "s1", "author": "User 1", "created_at": "2020-05-05T14:30:00.000Z" } ] } } } ``` After opening the generated document in Word, it displays that the suggestion was created on May 5, 2020, at 2:30:00 PM, regardless of the local time zone. With the time zone set to `America/Los_Angeles`, the previous suggestion would be displayed in Word as created on May 5, 2020, at 7:30:00 AM, regardless of the local time zone. The provided time zone must be a valid [IANA time zone](https://www.iana.org/time-zones) identifier. source file: "cs/latest/guides/export-to-word/content-formatting.html" ## Supported content formatting features Export to Word is capable of converting HTML content to a Word document while preserving the original formatting of the content. The converter supports a wide range of HTML elements and CSS properties, allowing for the conversion of complex content structures. This document provides an overview of the supported content formatting features, including text styles, inline elements, block elements, and common types of CSS properties. You can style HTML content using inline styles, styles defined inside the `

First paragraph with the default indentation and 1cm spacing.

Second paragraph with 4cm left indentation.

Third paragraph with the default indentation and 1cm spacing.

``` #### Background color It is possible to set the background color of a paragraph via the `background-color` property in the same way as the [background color of text](#cs/latest/guides/export-to-word/content-formatting.html--foreground-and-background-colors). ### Headings The converter supports all levels of HTML headings `

` to `

`. The headings are converted to corresponding Word Styles according to their level. See the [Word Styles](#cs/latest/guides/export-to-word/styles.html--word-styles) section for more information. Headings support the same content formatting as paragraphs. ### Lists The converter supports both ordered `
    ` and unordered `