diff --git a/docs/_snippets/builds/saving-data/autosave.html b/docs/_snippets/builds/saving-data/autosave.html new file mode 100644 index 00000000000..4f578e73cb8 --- /dev/null +++ b/docs/_snippets/builds/saving-data/autosave.html @@ -0,0 +1,111 @@ +
+

Test me!

+
+ +
+
+
Status:
+
+ + +
+
+ +
+
HTTP server lag (ms):
+ +
+
+ +

Server data:

+ +
+	
+
+ + diff --git a/docs/_snippets/builds/saving-data/autosave.js b/docs/_snippets/builds/saving-data/autosave.js new file mode 100644 index 00000000000..d189953b050 --- /dev/null +++ b/docs/_snippets/builds/saving-data/autosave.js @@ -0,0 +1,67 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document, setTimeout */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +let HTTP_SERVER_LAG = 500; + +document.querySelector( '#snippet-autosave-lag' ).addEventListener( 'change', evt => { + HTTP_SERVER_LAG = evt.target.value; +} ); + +ClassicEditor + .create( document.querySelector( '#snippet-autosave' ), { + cloudServices: CS_CONFIG, + toolbar: { + viewportTopOffset: 60 + }, + autosave: { + save( editor ) { + return saveData( editor.getData() ); + } + } + } ) + .then( editor => { + window.editor = editor; + + displayStatus( editor ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +function saveData( data ) { + return new Promise( resolve => { + // Fake HTTP server's lag. + setTimeout( () => { + log( data ); + + resolve(); + }, HTTP_SERVER_LAG ); + } ); +} + +function displayStatus( editor ) { + const pendingActions = editor.plugins.get( 'PendingActions' ); + const statusIndicator = document.querySelector( '.snippet-autosave-status' ); + const console = document.querySelector( '#snippet-autosave-console' ); + + pendingActions.on( 'change:hasAny', ( evt, propertyName, newValue ) => { + if ( newValue ) { + statusIndicator.classList.add( 'busy' ); + console.classList.remove( 'received' ); + } else { + statusIndicator.classList.remove( 'busy' ); + console.classList.add( 'received' ); + } + } ); +} + +function log( msg ) { + const console = document.querySelector( '#snippet-autosave-console' ); + console.textContent = msg; +} diff --git a/docs/_snippets/builds/saving-data/build-autosave-source.html b/docs/_snippets/builds/saving-data/build-autosave-source.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/_snippets/builds/saving-data/build-autosave-source.js b/docs/_snippets/builds/saving-data/build-autosave-source.js new file mode 100644 index 00000000000..ec7ee316dca --- /dev/null +++ b/docs/_snippets/builds/saving-data/build-autosave-source.js @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals window */ + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor'; + +import Autosave from '@ckeditor/ckeditor5-autosave/src/autosave'; + +ClassicEditor.builtinPlugins.push( Autosave ); + +window.ClassicEditor = ClassicEditor; diff --git a/docs/_snippets/builds/saving-data/manualsave.html b/docs/_snippets/builds/saving-data/manualsave.html new file mode 100644 index 00000000000..c3cc0da57c2 --- /dev/null +++ b/docs/_snippets/builds/saving-data/manualsave.html @@ -0,0 +1,92 @@ +
+

Test me!

+
+ +
+
+
HTTP server lag (ms):
+ +
+
+ +
+
+ +

Server data:

+ +
+	
+
+ + diff --git a/docs/_snippets/builds/saving-data/manualsave.js b/docs/_snippets/builds/saving-data/manualsave.js new file mode 100644 index 00000000000..b03b07e3fae --- /dev/null +++ b/docs/_snippets/builds/saving-data/manualsave.js @@ -0,0 +1,106 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document, setTimeout */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +let HTTP_SERVER_LAG = 500; +let isDirty = false; + +document.querySelector( '#snippet-manual-lag' ).addEventListener( 'change', evt => { + HTTP_SERVER_LAG = evt.target.value; +} ); + +ClassicEditor + .create( document.querySelector( '#snippet-manual' ), { + cloudServices: CS_CONFIG, + toolbar: { + viewportTopOffset: 60 + } + } ) + .then( editor => { + window.editor = editor; + + handleStatusChanges( editor ); + handleSaveButton( editor ); + handleBeforeunload( editor ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +// Handle clicking the "Save" button. +function handleSaveButton( editor ) { + const saveButton = document.querySelector( '#snippet-manual-save' ); + const pendingActions = editor.plugins.get( 'PendingActions' ); + + saveButton.addEventListener( 'click', evt => { + const data = editor.getData(); + const action = pendingActions.add( 'Saving in progress.' ); + + evt.preventDefault(); + + // Fake HTTP server's lag. + setTimeout( () => { + log( data ); + + pendingActions.remove( action ); + + // Reset isDirty only if data didn't change in the meantime. + if ( data == editor.getData() ) { + isDirty = false; + } + + updateStatus( editor ); + }, HTTP_SERVER_LAG ); + } ); +} + +function handleStatusChanges( editor ) { + const pendingActions = editor.plugins.get( 'PendingActions' ); + + pendingActions.on( 'change:hasAny', () => updateStatus( editor ) ); + + editor.model.document.on( 'change:data', () => { + isDirty = true; + + updateStatus( editor ); + } ); +} + +function handleBeforeunload( editor ) { + window.addEventListener( 'beforeunload', evt => { + if ( editor.plugins.get( 'PendingActions' ).hasAny ) { + evt.preventDefault(); + } + } ); +} + +function updateStatus( editor ) { + const saveButton = document.querySelector( '#snippet-manual-save' ); + + if ( isDirty ) { + saveButton.classList.add( 'active' ); + } else { + saveButton.classList.remove( 'active' ); + } + + if ( editor.plugins.get( 'PendingActions' ).hasAny ) { + document.querySelector( '#snippet-manual-save-console' ).classList.remove( 'received' ); + saveButton.value = 'Saving...'; + saveButton.classList.add( 'saving' ); + } else { + saveButton.value = 'Save'; + saveButton.classList.remove( 'saving' ); + } +} + +function log( msg ) { + const console = document.querySelector( '#snippet-manual-save-console' ); + + console.classList.add( 'received' ); + console.textContent = msg; +} diff --git a/docs/builds/guides/development/installing-plugins.md b/docs/builds/guides/development/installing-plugins.md index d0162d356eb..d57241905ba 100644 --- a/docs/builds/guides/development/installing-plugins.md +++ b/docs/builds/guides/development/installing-plugins.md @@ -186,35 +186,34 @@ All this works because a typical `src/ckeditor.js` module that you can find in e ```js import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'; -import UploadadapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; +import UploadAdapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; import AutoformatPlugin from '@ckeditor/ckeditor5-autoformat/src/autoformat'; import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'; import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import BlockquotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; // ... export default class ClassicEditor extends ClassicEditorBase {} -ClassicEditor.build = { - plugins: [ - EssentialsPlugin, - UploadadapterPlugin, - AutoformatPlugin, - BoldPlugin, - ItalicPlugin, - BlockquotePlugin, - // ... - ], - config: { - toolbar: { - items: [ - 'heading', - 'bold', - // ... - ] - }, - // ... - } +ClassicEditor.builtInPlugins = [ + EssentialsPlugin, + UploadAdapterPlugin, + AutoformatPlugin, + BoldPlugin, + ItalicPlugin, + BlockQuotePlugin, + // ... +]; + +ClassicEditor.defaultConfig = { + toolbar: { + items: [ + 'heading', + 'bold', + // ... + ] + }, + // ... }; ``` diff --git a/docs/builds/guides/integration/advanced-setup.md b/docs/builds/guides/integration/advanced-setup.md index 13600c4fb21..2221d17a0e9 100644 --- a/docs/builds/guides/integration/advanced-setup.md +++ b/docs/builds/guides/integration/advanced-setup.md @@ -1,6 +1,6 @@ --- category: builds-integration -order: 40 +order: 50 --- # Advanced setup @@ -175,71 +175,69 @@ You can now import all the needed plugins and the creator directly into your cod ```js import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'; -import UploadadapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; +import UploadAdapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; import AutoformatPlugin from '@ckeditor/ckeditor5-autoformat/src/autoformat'; import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'; import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import BlockquotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; -import EasyimagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import EasyImagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage'; import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading'; import ImagePlugin from '@ckeditor/ckeditor5-image/src/image'; -import ImagecaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'; -import ImagestylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'; -import ImagetoolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'; -import ImageuploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'; +import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'; +import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'; +import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'; +import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'; import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'; import ListPlugin from '@ckeditor/ckeditor5-list/src/list'; import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'; export default class ClassicEditor extends ClassicEditorBase {} -ClassicEditor.build = { - plugins: [ - EssentialsPlugin, - UploadadapterPlugin, - AutoformatPlugin, - BoldPlugin, - ItalicPlugin, - BlockquotePlugin, - EasyimagePlugin, - HeadingPlugin, - ImagePlugin, - ImagecaptionPlugin, - ImagestylePlugin, - ImagetoolbarPlugin, - ImageuploadPlugin, - LinkPlugin, - ListPlugin, - ParagraphPlugin - ], - config: { - toolbar: { - items: [ - 'heading', - '|', - 'bold', - 'italic', - 'link', - 'bulletedList', - 'numberedList', - 'imageUpload', - 'blockQuote', - 'undo', - 'redo' - ] - }, - image: { - toolbar: [ - 'imageStyle:full', - 'imageStyle:side', - '|', - 'imageTextAlternative' - ] - }, - language: 'en' - } +ClassicEditor.builtinPlugins = [ + EssentialsPlugin, + UploadAdapterPlugin, + AutoformatPlugin, + BoldPlugin, + ItalicPlugin, + BlockQuotePlugin, + EasyImagePlugin, + HeadingPlugin, + ImagePlugin, + ImageCaptionPlugin, + ImageStylePlugin, + ImageToolbarPlugin, + ImageUploadPlugin, + LinkPlugin, + ListPlugin, + ParagraphPlugin +]; + +ClassicEditor.defaultConfig = { + toolbar: { + items: [ + 'heading', + '|', + 'bold', + 'italic', + 'link', + 'bulletedList', + 'numberedList', + 'imageUpload', + 'blockQuote', + 'undo', + 'redo' + ] + }, + image: { + toolbar: [ + 'imageStyle:full', + 'imageStyle:side', + '|', + 'imageTextAlternative' + ] + }, + language: 'en' }; - ``` This module will export an editor creator class which has all the plugins and configuration that you need already built-in. To use such editor, simply import that class and call the static `.create()` method like in all {@link builds/guides/integration/basic-api#creating-an-editor examples}. @@ -265,18 +263,18 @@ The second variant how to run the editor is to use the creator class directly, w ```js import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'; -import UploadadapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; +import UploadAdapterPlugin from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; import AutoformatPlugin from '@ckeditor/ckeditor5-autoformat/src/autoformat'; import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'; import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import BlockquotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; -import EasyimagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import EasyImagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage'; import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading'; import ImagePlugin from '@ckeditor/ckeditor5-image/src/image'; -import ImagecaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'; -import ImagestylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'; -import ImagetoolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'; -import ImageuploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'; +import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'; +import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'; +import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'; +import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'; import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'; import ListPlugin from '@ckeditor/ckeditor5-list/src/list'; import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -289,12 +287,12 @@ ClassicEditor AutoformatPlugin, BoldPlugin, ItalicPlugin, - BlockquotePlugin, + BlockQuotePlugin, HeadingPlugin, ImagePlugin, - ImagecaptionPlugin, - ImagestylePlugin, - ImagetoolbarPlugin, + ImageCaptionPlugin, + ImageStylePlugin, + ImageToolbarPlugin, LinkPlugin, ListPlugin, ParagraphPlugin diff --git a/docs/builds/guides/integration/configuration.md b/docs/builds/guides/integration/configuration.md index d0b264b9cfd..1d73af525a5 100644 --- a/docs/builds/guides/integration/configuration.md +++ b/docs/builds/guides/integration/configuration.md @@ -59,7 +59,7 @@ ClassicEditor Each build has a number of plugins available. You can easily list all plugins available in your build: ```js -ClassicEditor.build.plugins.map( plugin => plugin.pluginName ); +ClassicEditor.builtinPlugins.map( plugin => plugin.pluginName ); ``` ## Adding features diff --git a/docs/builds/guides/integration/saving-data.md b/docs/builds/guides/integration/saving-data.md new file mode 100644 index 00000000000..46a1ace25c5 --- /dev/null +++ b/docs/builds/guides/integration/saving-data.md @@ -0,0 +1,313 @@ +--- +category: builds-integration +order: 40 +--- + +{@snippet builds/saving-data/build-autosave-source} + +# Getting and saving data + +CKEditor 5 allows you to retrieve and save the data to your server (or to your system in general) in various ways. In this guide you can learn about the options and their pros and cons. + +## Automatic integration with HTML forms + +This is the most classical way of integrating the editor. It is typically used in simpler CMSes, forums, comment sections, etc. + +This approach is only available in the {@link builds/guides/overview#classic-editor Classic editor} and only if it was used to replace a ` +

+ + + + +```` + +The Classic editor will automatically update the value of the ` + ``` + + Thanks to that, the ` + ``` + + Instead of being printed like this: + + ```html + + ``` + + While a simple content like mentioned above does not itself require to be encoded, encoding the data will prevent losing text like `<` or `<img>`. + +--> + +## Manually retrieving the data + +When you use AJAX requests instead of the classical integration with HTML forms, implement a single-page application or you use a different editor type than the Classic editor (and hence, you cannot use the previous method), you can retrieve the data from the editor by using the {@link module:editor-classic/classiceditor~ClassicEditor#getData `editor.getData()`} method. + +For that, you will need to store the reference to the `editor` because, unlike in CKEditor 4, there is no global `CKEDITOR.instances` property. You can do that in multiple ways, e.g. assigning the `editor` to a variable defined outside the `then()`'s callback: + +```js +let editor; + +ClassicEditor + .create( document.querySelector( '#editor' ) ) + .then( newEditor => { + editor = newEditor; + } ) + .catch( error => { + console.error( error ); + } ); + +// Assuming there's a in your application. +document.querySelector( '#submit' ).addEventListener( 'click', () => { + const editorData = editor.getData(); + + // ... +} ); +``` + +## Autosave feature + +The {@link module:autosave/autosave~Autosave} allows you to automatically save the data (e.g. send it to the server) when needed (when the user changed the content). + + + This plugin is not available in any of the builds by default so you need to {@link builds/guides/development/installing-plugins install it}. + + +Assuming that you implemented a `saveData()` function which sends the data to your server and returns a promise which is resolved once the data is successfully saved, configuring the autosave feature is as simple as: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Autosave, + + // ... other plugins + ], + + autosave: { + save( editor ) { + return saveData( editor.getData() ); + } + }, + + // ... other configuration options + } ); +``` + +The autosave feature listens to the {@link module:engine/model/document~Document#event:change:data `editor.model.document#change:data`} event, throttles it and executes the `config.autosave.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 was not called yet due to throttling), +* or any of the editor features registered a {@link module:core/pendingactions~PendingActions "pending action"} (e.g. 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. + +### Demo + +This demo shows a simple integration of the editor with a fake HTTP server (which needs 1000ms to save the content). + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + 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. +function saveData( data ) { + return new Promise( resolve => { + + setTimeout( () => { + console.log( data ); + + resolve(); + }, HTTP_SERVER_LAG ); + } ); +} + +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' ); + } + } ); +} +``` + +How to understand this demo: + +* The status indicator shows when the editor has some unsaved content or pending actions. + * If you would drop a big image into this editor you will see that it is busy during the entire period while the image is being uploaded. + * The editor is busy also when saving the content is in progress (the `save()`'s promise was not resolved). +* The autosave feature will throttle changes so frequent changes (e.g. typing) are grouped in batches. +* The autosave does not check itself whether the data really changed. It bases on changes in the model which, in special cases, 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 in a row. +* 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 (e.g. 9000ms) and typing something. Those actions will make the editor "busy" for a longer time – try leaving the page at that moments. + +{@snippet builds/saving-data/autosave} + +## Handling users exiting the page + +The additional concern when integrating the editor in your website is that the user may mistakenly leave before saving the data. This problem is automatically handled by the [autosave feature](#autosave-feature) described above, but if you do not use it and instead chose different integration methods, you should consider handling these two scenarios: + +* The user leaves the page before saving the data (e.g. mistakenly closes a tab or clicks some link). +* The user saved the data, but there are some pending actions like an image upload. + +To handle the former situation you can listen to the native [`window#beforeunload`](https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload) event. The latter situation can be handled by using CKEditor 5's {@link module:core/pendingactions~PendingActions} plugin. + +### Demo + +The below example shows how all these mechanism can be used together to enable/disable a "Save" button and blocking the user from leaving the page without saving the data. + +```js +let isDirty = false; + +ClassicEditor + .create( document.querySelector( '#editor' ) ) + .then( editor => { + window.editor = editor; + + handleStatusChanges( editor ); + handleSaveButton( editor ); + handleBeforeunload( editor ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +// Handle clicking the "Save" button. +function handleSaveButton( editor ) { + const saveButton = document.querySelector( '#save' ); + const pendingActions = editor.plugins.get( 'PendingActions' ); + + saveButton.addEventListener( 'click', evt => { + const data = editor.getData(); + const action = pendingActions.add( 'Saving changes' ); + + evt.preventDefault(); + + // Fake HTTP server's lag. + setTimeout( () => { + log( data ); + + pendingActions.remove( action ); + + // Reset isDirty only if data didn't change in the meantime. + if ( data == editor.getData() ) { + isDirty = false; + } + + updateStatus( editor ); + }, HTTP_SERVER_LAG ); + } ); +} + +function handleStatusChanges( editor ) { + const pendingActions = editor.plugins.get( 'PendingActions' ); + + pendingActions.on( 'change:hasAny', () => updateStatus( editor ) ); + + editor.model.document.on( 'change:data', () => { + isDirty = true; + + updateStatus( editor ); + } ); +} + +function handleBeforeunload( editor ) { + const pendingActions = editor.plugins.get( 'PendingActions' ); + + window.addEventListener( 'beforeunload', evt => { + if ( pendingActions.hasAny ) { + evt.returnValue = pendingActions.first.message; + } + } ); +} + +function updateStatus( editor ) { + const saveButton = document.querySelector( '#save' ); + + if ( isDirty ) { + saveButton.classList.add( 'active' ); + } else { + saveButton.classList.remove( 'active' ); + } + + if ( editor.plugins.get( 'PendingActions' ).hasAny ) { + saveButton.classList.add( 'saving' ); + } else { + saveButton.classList.remove( 'saving' ); + } +} +``` + +How to understand this demo: + +* The "Save" button becomes active when there are some changes to be saved. +* The spinner is shown when the data is being sent to the server or there are any other pending actions (e.g. image being uploaded). +* 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 (e.g. 9000ms) and clicking the "Save" button. Those actions will make the editor "busy" for a longer time – try leaving the page at that moments. + +{@snippet builds/saving-data/manualsave} diff --git a/docs/framework/guides/overview.md b/docs/framework/guides/overview.md index 597b3f3d441..1cd2a802545 100644 --- a/docs/framework/guides/overview.md +++ b/docs/framework/guides/overview.md @@ -25,7 +25,7 @@ The framework was designed to be a highly flexible and universal platform for cr * **Plugin-based architecture.** Everything is a plugin — even such crucial features as support for typing or `

` elements. You can remove plugins or replace them with your own implementations to achieve fully customized results. * **Schema-less core.** The core makes minimal assumptions and can be controlled through the schema. This leaves all decisions to plugins and hence to you. -* **Collaboration-ready.** Or rather, real-time collaboration is **ready for you to use**! The editor implements [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation) for the tree-structured model as well as many other mechanisms which were required to create a seamless collaborative UX. Additionally, we provide cloud infrastructure and plugins enabling real-time collaborative editing in your application! {@link @ckeditor5 features/overvie Check the collaboration demo}. +* **Collaboration-ready.** Or rather, real-time collaboration is **ready for you to use**! The editor implements [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation) for the tree-structured model as well as many other mechanisms which were required to create a seamless collaborative UX. Additionally, we provide cloud infrastructure and plugins enabling real-time collaborative editing in your application! {@link @ckeditor5 features/overview Check the collaboration demo}. * **Custom data model.** The editing engine implements a tree-structured custom data model, designed to fit multiple requirements such as enabling real-time collaboration and complex editing features (like tables or nested blocks). * **Virtual DOM.** The editing engine features a custom, editing-oriented virtual DOM implementation that aims to hide browser quirks from your sight. **No more `contentEditable` nightmares!** * **Granular, reusable features.** Features are implemented in a granular way. This allows for reusing and recomposing them which, in turn, makes it possible to customize and extend the editor. For instance, the {@link features/image image feature} consists of over 10 plugins at the moment.