diff --git a/.travis.yml b/.travis.yml index b9bdae0..a7b4930 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,5 @@ after_success: - ckeditor5-dev-tests-save-revision env: global: - - secure: yGjqvN+FNsbRuAneBgIi9AyraRbhCSm8MEd7FPq/exSTjfsWofOq5i9O7o7/02bJVZ8rveD71OaqAKQCWVG8BD4umbpu0N0PpkJbjVi8jmytzN28mUxgrmZkFbr96C5YnGqvdxf4AB9wT5ExtzTfp5XUO383/WoGPu9qt6rmb5II1PHwhT1WzXql/pCU5eGLccCYtlEr3ybWD9n3wiC4fM90M5axuSu9EwpvTEfMJQbeqEv7jSu+qlPUAJ4rSTmgt1pGIMxJgUpDi76qnn4VdTxRUZy+4heAFT9c/96tPV0QAz8cpVM3qGKSTuF8FAY71sddF4PX8wfG/1GjO+dHEmbop5Kz8UXnS8rnp9gx4Q5be8iW0DfPCqjc8a2ULWgUMr4jBnbZKpMHNT83Li1TfGflIiDksh68EKMRHHoVzxKJ2ysBSMzPqLfLXSyY9Jl/j5OBAiVl1n22oq5t9AlT6JEm0bYcgIwVIRpJqPMZMkZBCoLZHSyVfdIqdT+bcNgSEG5iLrXS9FlqsOS9FB4U3DbvPZbNBq+uoEuAyk5w+T5OkqIhXBddUG9emaa6TuQNNcATyucwWxB3HC1UAK8s5XTWVUzQ86nUShzH7aHJky+PJN0hkYAxwjldwtzTWwKdw+SBjk3Cl5h8JEepJSX0Lqtwci4XGoHsJ4JL5UCU5OM= - - secure: KDj0ramEp45sMIyRogmNesjH5O2q7k5kopvDjG5+O1PWjMc+moxoSLYdcqknINLdHkid/SfwyfDppO6rvsVuLR9nvq7SPsM/xDGtLMoLYwUoWPrK57tQ1exl1FUWHzZs/7UJOwC+GnqqtL6bN6PAAj4aSLoTai/xDiDBTRMrVzDmYGeHaHihI44jtqsfBEkCGd9vfB0JDZvhncZqQrEubTwFnh+QwZXTnVR837p+EWhT87HvH/OPioXPnEF3O3N72u/WpqmQKrgM2vUWOfjHzZTEleBBJZnbrFFKzoOczig2a4I0hLiaqtK/bsSpjMpyKVzW+Jh+bQiKLk2B3DlUL+/6RH/dioo0DM438dI5sT46dNDl8k3mXCfJyGpn1by3EKdjEkkgLz095YzaCIEKBLVqKky4FhqRduEGpmXXMJw5xJQuaLOqC8jg0bgAl/E6q0uMyVPJuaC1ewGOHSx33j09ZXrOCAk57KzXgJzVi3BJR2kf4x2KygRqqVtFVYjOyvdAS2EieytMKwMVfQfrtmgJU5KCcbYzNX6ZEKEdS7SxTlyPtDdJssxvuuRU8xxgkswBxTkFGFG77dy0BTsa0xmAdiJnb/Z3F9hJP3VySOKj/lnATO3Yzr5v5l0jD1SQqfG9IYvDE/fDULs7/cFuj46voHIVBkziQ+IAqv/3vYo= - - secure: Gx267GbmaD2jMWCirha+1wQ/A7kJnHMGIkzUUBgzsy8CszGfQv8WZe8KnSeiLjV/+/gXg9v+/0LPULDjs9e2d8FdpGdA+7iE3HPCXijR/LWCJq9ZiMN6QrCSaLI5o6mpkUB29sfgKOVfC6CEjyd00sK3LzcjSJeApSgPY90g1FOsbMg/t1PIzFUrJytvFnjUr2cOPay2ZeXrXFLox2je8n1YvtCmWt6QgMGmT0bBQRjYFHhiw7IsGZ9aT36Um4qx9hXADixIXfE+RRxhdrVcFhcmESfr7US9inZKMDGQa48VYUX8kdPr073bi9Wh8c5WsmiIT4cspviyLhWfHvq5hwhGt1FTbfcuQIeq172hFoLRphwWujENrzJnXjSbKIDFHqv08p0TJJLe/Q2af0imeWSL9+hszIN7Z7S12VYxbixAMmxaB9DXhqdVN7qhNKV4e/KhZv+60DLUPIrP7iLYNpb2TK1D9tr5rnF3v0FbJTCCr1XyCRbZSdGi9HnAm5bw7n5rllCW7NB6QUIPrBwBu7cJ37PqFeJXZDIXlmBEGP+ZBV4u7vIBBO8CXaaVCEvflIGqE15PbGvSc8wPiP4X4tLjnpgUY6OP67sCNRbuH0XeuvhGPRf3C15GNp14qM7cFtmoKSKxy+sUQBtx5LCpESI/JMEUiotqk232l0vqyR8= - - secure: eqj8DaCp7F6/XXy6npJ4YO7jfQaAbMBYo2rsvYAk0lubYSL0fJsbtMR1Qm8kbXFENxFKhpcU6fI9pAPKBT2fvn3/dnIHTng4xklpWWdl8CgojGJ8wPu/RjTa9UxDXvsDaiixipg/uPfc9oReCzEhulNVB6V6iq9jHeBJaQqgI5R1b3GB3U/sIF+Y7vWhICjM9s7WE/ESqADVHMpSQnzMIAiFJwJgqB1zu9bkwSsjhMgNscgifWuB8+AoXVeCesSYyJQsG7PHSoY+JlzGUw05xtSc6x+3bivs7dyrt68bkaFkVN2YmAstxl45R0Itd5dgPt/bvXKo63CTtkNAsyvUrK8Z3F2VqnqqTJ+3exQbkHCxHW2Pd7QOyxVAy00OCdIcjEAtSQp5HrI9ZAipP/9JJq5KzdaNzpSLwuL6jt9WysnMQBF3LMB1ONWRlrCcGdkUfTt2cjBMR3yiaaPJK+Bk93eSNPDJ9BaQbIGtsI6p2aojlgYzz7Uvon9d1lY9mlZi5BnyWwiOzfpmdALZlnl9yl6Bh/PI7SfW0zWRYOWS33pSWt9nkoNre9uTOdAhlJpS1PohNraEzw1AYR1j6hRZszqpf8JpMIrdoUOdf4QMh1LIJZW7EbEULsvU9fDzLZucjQyYpZLvFjXQ15UR+u4iTsC8c9rNGdDJkuBAsg81zis= + - secure: czAq3wmkippaXuAaoiibg+6vtcfF6ylU6QgzLIABhegKQaOARSB6Cae7WeveCBKu03aiVxWSTzPtAkNnq4qGK8mxEh012r1F50zKBInR9QiEB8HHkiUrkO2cVt21hX/PzdCJ+Ys4Cjthm1xsQwZ8vrfXsZnyahkQCzLxqkQR7mYNiQGPRaGvxaJmgMlfNNgoRFO7KzcmTNX9OeU+4k2BJOuua8NuoZx01aCktaC6qGpYLFE9Kj+oDpWMcQXp8y20hIDeY7GOh2zRDlQqXQV9FslxOykHE0uJ2dRa6Wvf65PTQknrpkBLHQgdaIM0e/E4+xtKMxl7676P1gbpQAntyCD8QybE3N+ldJqrOzzLmQ5hTxKjHxzU3/rtXqowRCtJS1a9/gV6ABbE3PKzfBi1fhkx7uyzc84owKTuR8VMTuXwPqjAuBbbD2Limv1GbS+Of6VPlaM7FGg22ZIjWr5H/kEIydvB9+9ffIOCjwi+5LObss1MiqZTn6tYwJvq6ELTj4a/6rXF4r2G4GLqPfheNxad3FXrTGsT2BYkrp8J1YiNwHVPnaec16S8tzRS7dQjKQiTinp/3wtOsdJ87XY+Z0wKFwv1nvn9wFWpTLo2Lh9a1grxW05HvrrKr0MIwlVdieHHsjtaBMYbwS1WtIoOMA1znsghM6ZPUVGzYX5mn5w= + - secure: VJkWZMdkuuvwdHMyhJdTiJ9jjAtuVOQn7sQjpBAPzfLyjE7DliaS0gYHz5KG8RLs1Qn99ShNMr6PXcuZ7tMVF4YMSLU4AHh4+wg+EYXxWDbiiz3AbSPa2ScRVQrqayEdvdHv77ydPSXRj7wY2wWzpSQml6zdPHNhLRi9czKxPmehEloZKZfBiJY9/CmeiIF/Fv1KK/OsuOgafyOOG/ffJeqiI4agYW8PgxrMRl71YITl9IOWDZw093x4AqlXFG8JUxYk0JqiSJDWJCSxID7A/12lOe/ExsPPRQ69EKXMsMxsVLKhNlHpo3GHMXO0bf461Y0Y/1exCbeKP+F2ILaBJOKxhPD6ulWLTBcB/58XV/ke8G8cwYyFKl3nNxcejK1bR8fE9/4q+vsjnCTNq4tJxRN1RW324uC7AbL8f/Mur5TIWUGftmjhFLU+bjKkuTADhbwB9XYU2KhmAN96fpEvQ4RIz4/nIs9dbfKY/Hj2YAyblfXeWLMPxmhOXTScR7dkw+TWDiU2gUCw2MjU8AyOhB3Iwenxgpe+jGObvt+zPZz8UWgyJYb+KtZsNlmr+vvISa5m+jAukOcY2afH0tH/mMvwdXntLH7A+X40BWrP/14cA609clyw5ou5Jaxo+XPVX0GAsR/B99NkJbUGLKUgGP4fWf3WfGra1WVZZ9PKmuo= diff --git a/docs/framework/guides/document-editor.md b/docs/framework/guides/document-editor.md index 76ec3d5..92a8cd2 100644 --- a/docs/framework/guides/document-editor.md +++ b/docs/framework/guides/document-editor.md @@ -5,7 +5,7 @@ order: 30 # Document editor -The {@link examples/builds/document-editor document editor example} showcases the {@link builds/guides/quick-start#document-editor document editor build} designed for document editing with a customized UI representing the layout of a sheet of paper. It was created on top of the {@link module:editor-decoupled/decouplededitor~DecoupledEditor `DecoupledEditor`} and makes the best of what it offers: the freedom to choose the location of the crucial UI elements in the application. +The {@link examples/builds/document-editor document editor example} showcases the {@link builds/guides/quick-start#document-editor document editor build} designed for document editing with a customized UI representing the layout of a sheet of paper. It was created on top of the {@link module:editor-decoupled/decouplededitor~DecoupledEditor `DecoupledEditor`} class and makes the best of what it offers: the freedom to choose the location of the crucial UI elements in the application. In this tutorial you will learn how to create your own document editor with a customized user interface, step–by–step. @@ -13,27 +13,32 @@ In this tutorial you will learn how to create your own document editor with a cu ## The editor -The `DecoupledDocumentEditor` includes all the necessary features for the task. All you need to do is import it and create a new instance. +The document editor build includes all the necessary features for the task. All you need to do is import it and create a new instance. See the {@link builds/guides/quick-start#document-editor quick start guide} to learn how to install the document editor build. -Unlike the {@link builds/guides/overview#classic-editor classic editor}, the document editor does not require any data container in the DOM. Instead, it accepts a string containing the initial data as the first argument of the static `create()` method. To get the output data, use the {@link module:core/editor/utils/dataapimixin~DataApi#getData `getData`} method. +The document editor can be created using the existing data container in the DOM. It can also accept a raw data string and create the editable by itself. To get the output data, use the {@link module:core/editor/utils/dataapimixin~DataApi#getData `getData()`} method. + + + See the {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} to learn about different approaches to the initialization of the editor. + ```js import DecoupledDocumentEditor from '@ckeditor/ckeditor5-build-decoupled-document/src/ckeditor'; DecoupledDocumentEditor - .create( '

Initial editor data.

', { - toolbarContainer: document.querySelector( '.document-editor__toolbar' ), - editableContainer: document.querySelector( '.document-editor__editable' ), - + .create( document.querySelector( '.document-editor__editable' ), { cloudServices: { .... } } ) .then( editor => { + const toolbarContainer = document.querySelector( '.document-editor__toolbar' ); + + toolbarContainer.appendChild( editor.ui.view.toolbar.element ); + window.editor = editor; } ) .catch( err => { @@ -41,9 +46,7 @@ DecoupledDocumentEditor } ); ``` -You may have noticed two configuration options used here: {@link module:core/editor/editorconfig~EditorConfig#toolbarContainer `config.toolbarContainer`} and {@link module:core/editor/editorconfig~EditorConfig#editableContainer `config.editableContainer`}. They specify the location of the editor toolbar and editable in your application. - -If you do not specify these configuration options, you have to make sure the editor UI is injected into your application after it fires the {@link module:core/editor/editorwithui~EditorWithUI#event:uiReady `uiReady`} event. The toolbar element is accessible via `editor.ui.view.toolbar.element` and the editable element can be found under `editor.ui.view.editable.element`. +You may have noticed that you have to make sure the editor UI is injected into your application after it fires the {@link module:core/editor/editorwithui~EditorWithUI#event:uiReady `Editor#uiReady`} event. The toolbar element can be found under `editor.ui.view.toolbar.element`. The document editor supports the Easy Image plugin provided by [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/) out of the box. Please refer to the {@link features/image-upload#easy-image documentation} to learn more. @@ -60,7 +63,11 @@ The following structure has two containers that correspond to the configuration ```html
-
+
+
+

The initial editor data.

+
+
``` @@ -114,7 +121,7 @@ The editable should look like a sheet of paper, centered in its scrollable conta ```css /* Make the editable container look like the inside of a native word processor application. */ -.document-editor__editable { +.document-editor__editable-container { padding: calc( 2 * var(--ck-spacing-large) ); background: var(--ck-color-base-foreground); @@ -122,7 +129,7 @@ The editable should look like a sheet of paper, centered in its scrollable conta overflow-y: scroll; } -.document-editor__editable .ck-editor__editable { +.document-editor__editable-container .ck-editor__editable { /* Set the dimensions of the "page". */ width: 15.8cm; min-height: 21cm; diff --git a/src/decouplededitor.js b/src/decouplededitor.js index 212e89a..a2061fd 100644 --- a/src/decouplededitor.js +++ b/src/decouplededitor.js @@ -12,7 +12,10 @@ import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor'; import DecoupledEditorUI from './decouplededitorui'; import DecoupledEditorUIView from './decouplededitoruiview'; +import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement'; +import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement'; /** * The {@glink builds/guides/overview#decoupled-editor decoupled editor} implementation. @@ -28,11 +31,11 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; * In order to create a decoupled editor instance, use the static * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} method. * - * # Decoupled editor and document build + * # Decoupled editor and document editor build * * The decoupled editor can be used directly from source (if you installed the * [`@ckeditor/ckeditor5-editor-decoupled`](https://www.npmjs.com/package/@ckeditor/ckeditor5-editor-decoupled) package) - * but it is also available in the {@glink builds/guides/overview#document-editor document build}. + * but it is also available in the {@glink builds/guides/overview#document-editor document editor build}. * * {@glink builds/guides/overview Builds} are ready-to-use editors with plugins bundled in. When using the editor from * source you need to take care of loading all plugins by yourself @@ -54,44 +57,80 @@ export default class DecoupledEditor extends Editor { * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} method instead. * * @protected - * @param {String} data The data to be loaded into the editor. + * @param {HTMLElement|String} elementOrData The DOM element that serves as an editable. + * The data will be loaded from it and loaded back to it once the editor is destroyed. + * Alternatively, a data string to be loaded into the editor. * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration. */ - constructor( config ) { + constructor( elementOrData, config ) { super( config ); + if ( isElement( elementOrData ) ) { + /** + * The element used as an editable. The data will be loaded from it and loaded back to + * it once the editor is destroyed. + * + * **Note:** The property is available only when such element has been passed + * to the {@link #constructor}. + * + * @readonly + * @member {HTMLElement} + */ + this.element = elementOrData; + } + this.data.processor = new HtmlDataProcessor(); this.model.document.createRoot(); - this.ui = new DecoupledEditorUI( this, new DecoupledEditorUIView( this.locale ) ); + this.ui = new DecoupledEditorUI( this, new DecoupledEditorUIView( this.locale, this.element ) ); } /** * Destroys the editor instance, releasing all resources used by it. * + * **Note**: The decoupled editor does not remove the toolbar and editable when destroyed. You can + * do that yourself in the destruction chain: + * + * editor.destroy() + * .then( () => { + * // Remove the toolbar from DOM. + * editor.ui.view.toolbar.element.remove(); + * + * // Remove the editable from DOM. + * editor.ui.view.editable.element.remove(); + * + * console.log( 'Editor was destroyed' ); + * } ); + * * @returns {Promise} */ destroy() { + // Cache the data, then destroy. + // It's safe to assume that the model->view conversion will not work after super.destroy(). + const data = this.getData(); + this.ui.destroy(); - return super.destroy(); + return super.destroy() + .then( () => { + if ( this.element ) { + setDataInElement( this.element, data ); + } + } ); } /** * Creates a decoupled editor instance. * - * Creating an instance when using the {@glink builds/index CKEditor build}: + * Creating an instance when using the {@glink builds/index CKEditor 5 build}: * * DecoupledEditor - * .create( '

Editor data

', { - * // The location of the toolbar in the DOM. - * toolbarContainer: document.querySelector( 'body div.toolbar-container' ), - * - * // The location of the editable in the DOM. - * editableContainer: document.querySelector( 'body div.editable-container' ) - * } ) + * .create( document.querySelector( '#editor' ) ) * .then( editor => { + * // Append the toolbar to the element. + * document.body.appendChild( editor.ui.view.toolbar.element ); + * * console.log( 'Editor was initialized', editor ); * } ) * .catch( err => { @@ -107,48 +146,48 @@ export default class DecoupledEditor extends Editor { * import ... * * DecoupledEditor - * .create( '

Editor data

', { + * .create( document.querySelector( '#editor' ), { * plugins: [ Essentials, Bold, Italic, ... ], - * toolbar: [ 'bold', 'italic', ... ], - * - * // The location of the toolbar in the DOM. - * toolbarContainer: document.querySelector( 'div.toolbar-container' ), - * - * // The location of the editable in the DOM. - * editableContainer: document.querySelector( 'div.editable-container' ) + * toolbar: [ 'bold', 'italic', ... ] * } ) * .then( editor => { + * // Append the toolbar to the element. + * document.body.appendChild( editor.ui.view.toolbar.element ); + * * console.log( 'Editor was initialized', editor ); * } ) * .catch( err => { * console.error( err.stack ); * } ); * - * **Note**: The {@link module:core/editor/editorconfig~EditorConfig#toolbarContainer `config.toolbarContainer`} and - * {@link module:core/editor/editorconfig~EditorConfig#editableContainer `config.editableContainer`} settings are optional. - * It is possible to define the location of the UI elements manually once the editor is up and running: + * **Note**: It is possible to create the editor out of a pure data string. The editor will then render + * an editable element that must be inserted into the DOM for the editor to work properly: * * DecoupledEditor * .create( '

Editor data

' ) * .then( editor => { - * console.log( 'Editor was initialized', editor ); - * - * // Append the toolbar and editable straight into the element. + * // Append the toolbar to the element. * document.body.appendChild( editor.ui.view.toolbar.element ); + * + * // Append the editable to the element. * document.body.appendChild( editor.ui.view.editable.element ); + * + * console.log( 'Editor was initialized', editor ); * } ) * .catch( err => { * console.error( err.stack ); * } ); * - * @param {String} data The data to be loaded into the editor. + * @param {HTMLElement|String} elementOrData The DOM element that serves as an editable. + * The data will be loaded from it and loaded back to it once the editor is destroyed. + * Alternatively, a data string to be loaded into the editor. * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration. * @returns {Promise} A promise resolved once the editor is ready. * The promise returns the created {@link module:editor-decoupled/decouplededitor~DecoupledEditor} instance. */ - static create( data, config ) { + static create( elementOrData, config ) { return new Promise( resolve => { - const editor = new this( config ); + const editor = new this( elementOrData, config ); resolve( editor.initPlugins() @@ -156,8 +195,9 @@ export default class DecoupledEditor extends Editor { editor.ui.init(); editor.fire( 'uiReady' ); } ) - .then( () => editor.editing.view.attachDomRoot( editor.ui.view.editableElement ) ) - .then( () => editor.data.init( data ) ) + .then( () => { + editor.data.init( editor.element ? getDataFromElement( editor.element ) : elementOrData ); + } ) .then( () => { editor.fire( 'dataReady' ); editor.fire( 'ready' ); @@ -169,51 +209,3 @@ export default class DecoupledEditor extends Editor { } mix( DecoupledEditor, DataApiMixin ); - -/** - * The configuration of the {@link module:editor-decoupled/decouplededitor~DecoupledEditor}. - * - * When specified, it controls the location of the {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#toolbar}: - * - * DecoupledEditor - * .create( '

Hello world!

', { - * // Append the toolbar to the element. - * toolbarContainer: document.body - * } ) - * .then( editor => { - * console.log( editor ); - * } ) - * .catch( error => { - * console.error( error ); - * } ); - * - * **Note**: If not specified, the toolbar must be manually injected into the DOM. See - * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} - * to learn more. - * - * @member {HTMLElement} module:core/editor/editorconfig~EditorConfig#toolbarContainer - */ - -/** - * The configuration of the {@link module:editor-decoupled/decouplededitor~DecoupledEditor}. - * - * When specified, it controls the location of the {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#editable}: - * - * DecoupledEditor - * .create( '

Hello world!

', { - * // Append the editable to the element. - * editableContainer: document.body - * } ) - * .then( editor => { - * console.log( editor ); - * } ) - * .catch( error => { - * console.error( error ); - * } ); - * - * **Note**: If not specified, the editable must be manually injected into the DOM. See - * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} - * to learn more. - * - * @member {HTMLElement} module:core/editor/editorconfig~EditorConfig#editableContainer - */ diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index f0ae78b..c36a581 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -52,22 +52,6 @@ export default class DecoupledEditorUI { * @private */ this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) ); - - /** - * The container for the {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#toolbar}. - * - * @type {HTMLElement|String} - * @private - */ - this._toolbarContainer = editor.config.get( 'toolbarContainer' ); - - /** - * The container for the {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#editable}. - * - * @type {HTMLElement|String} - * @private - */ - this._editableContainer = editor.config.get( 'editableContainer' ); } /** @@ -83,19 +67,12 @@ export default class DecoupledEditorUI { const editingRoot = editor.editing.view.document.getRoot(); view.editable.bind( 'isReadOnly' ).to( editingRoot ); view.editable.bind( 'isFocused' ).to( editor.editing.view.document ); + editor.editing.view.attachDomRoot( view.editableElement ); view.editable.name = editingRoot.rootName; this.focusTracker.add( this.view.editableElement ); this.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory ); - if ( this._toolbarContainer ) { - this._toolbarContainer.appendChild( view.toolbar.element ); - } - - if ( this._editableContainer ) { - this._editableContainer.appendChild( view.editable.element ); - } - enableToolbarKeyboardFocus( { origin: editor.editing.view, originFocusTracker: this.focusTracker, @@ -108,6 +85,6 @@ export default class DecoupledEditorUI { * Destroys the UI. */ destroy() { - this.view.destroy( !!this._toolbarContainer, !!this._editableContainer ); + this.view.destroy(); } } diff --git a/src/decouplededitoruiview.js b/src/decouplededitoruiview.js index 4e8dd74..0b6ab77 100644 --- a/src/decouplededitoruiview.js +++ b/src/decouplededitoruiview.js @@ -18,9 +18,8 @@ import Template from '@ckeditor/ckeditor5-ui/src/template'; * {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#toolbar}, but without any * specific arrangement of the components in the DOM. * - * See {@link module:core/editor/editorconfig~EditorConfig#toolbarContainer `config.toolbarContainer`} and - * {@link module:core/editor/editorconfig~EditorConfig#editableContainer `config.editableContainer`} to - * learn more about the UI of the decoupled editor. + * See {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} + * to learn more about this view. * * @extends module:ui/editorui/editoruiview~EditorUIView */ @@ -29,8 +28,9 @@ export default class DecoupledEditorUIView extends EditorUIView { * Creates an instance of the decoupled editor UI view. * * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance. + * @param {HTMLElement} [editableElement] The DOM element to be used as editable. */ - constructor( locale ) { + constructor( locale, editableElement ) { super( locale ); /** @@ -47,37 +47,22 @@ export default class DecoupledEditorUIView extends EditorUIView { * @readonly * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView} */ - this.editable = new InlineEditableUIView( locale ); + this.editable = new InlineEditableUIView( locale, editableElement ); // This toolbar may be placed anywhere in the page so things like font size need to be reset in it. + // Also because of the above, make sure the toolbar supports rounded corners. Template.extend( this.toolbar.template, { attributes: { - class: 'ck-reset_all' + class: [ + 'ck-reset_all', + 'ck-rounded-corners' + ] } } ); this.registerChildren( [ this.toolbar, this.editable ] ); } - /** - * Destroys the view and removes the {@link #toolbar} and {@link #editable} - * {@link module:ui/view~View#element `element`} from the DOM, if required. - * - * @param {Boolean} [removeToolbar] When `true`, remove the {@link #toolbar} element from the DOM. - * @param {Boolean} [removeEditable] When `true`, remove the {@link #editable} element from the DOM. - */ - destroy( removeToolbar, removeEditable ) { - super.destroy(); - - if ( removeToolbar ) { - this.toolbar.element.remove(); - } - - if ( removeEditable ) { - this.editable.element.remove(); - } - } - /** * @inheritDoc */ diff --git a/tests/decouplededitor.js b/tests/decouplededitor.js index b96d651..7f2450b 100644 --- a/tests/decouplededitor.js +++ b/tests/decouplededitor.js @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md. */ +/* globals document */ + import DecoupledEditorUI from '../src/decouplededitorui'; import DecoupledEditorUIView from '../src/decouplededitoruiview'; @@ -19,16 +21,19 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; testUtils.createSinonSandbox(); -describe( 'DecoupledEditor', () => { - let editor, editorData; +const editorData = '

foo bar

'; - beforeEach( () => { - editorData = '

foo bar

'; - } ); +describe( 'DecoupledEditor', () => { + let editor; describe( 'constructor()', () => { beforeEach( () => { editor = new DecoupledEditor(); + editor.ui.init(); + } ); + + afterEach( () => { + return editor.destroy(); } ); it( 'uses HTMLDataProcessor', () => { @@ -52,173 +57,214 @@ describe( 'DecoupledEditor', () => { } ); describe( 'create()', () => { - beforeEach( () => { - return DecoupledEditor - .create( editorData, { - plugins: [ Paragraph, Bold ] - } ) - .then( newEditor => { - editor = newEditor; - } ); + describe( 'editor with data', () => { + test( () => editorData ); } ); - afterEach( () => { - return editor.destroy(); - } ); + describe( 'editor with editable element', () => { + let editableElement; - it( 'creates an instance which inherits from the DecoupledEditor', () => { - expect( editor ).to.be.instanceof( DecoupledEditor ); - } ); + beforeEach( () => { + editableElement = document.createElement( 'div' ); + editableElement.innerHTML = editorData; + } ); - it( 'loads the initial data', () => { - expect( editor.getData() ).to.equal( '

foo bar

' ); + test( () => editableElement ); } ); - // #53 - it( 'creates an instance of a DecoupledEditor child class', () => { - class CustomDecoupledEditor extends DecoupledEditor {} - - return CustomDecoupledEditor - .create( editorData, { - plugins: [ Paragraph, Bold ] - } ) - .then( newEditor => { - expect( newEditor ).to.be.instanceof( CustomDecoupledEditor ); - expect( newEditor ).to.be.instanceof( DecoupledEditor ); + function test( getElementOrData ) { + it( 'creates an instance which inherits from the DecoupledEditor', () => { + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ Paragraph, Bold ] + } ) + .then( newEditor => { + expect( newEditor ).to.be.instanceof( DecoupledEditor ); - expect( newEditor.getData() ).to.equal( '

foo bar

' ); - - return newEditor.destroy(); - } ); - } ); - - // https://github.com/ckeditor/ckeditor5-editor-decoupled/issues/3 - it( 'initializes the data controller', () => { - let dataInitSpy; + return newEditor.destroy(); + } ); + } ); - class DataInitAssertPlugin extends Plugin { - constructor( editor ) { - super(); + it( 'loads the initial data', () => { + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ Paragraph, Bold ] + } ) + .then( newEditor => { + expect( newEditor.getData() ).to.equal( '

foo bar

' ); - this.editor = editor; - } + return newEditor.destroy(); + } ); + } ); - init() { - dataInitSpy = sinon.spy( this.editor.data, 'init' ); - } - } + // https://github.com/ckeditor/ckeditor5-editor-classic/issues/53 + it( 'creates an instance of a DecoupledEditor child class', () => { + class CustomDecoupledEditor extends DecoupledEditor {} - return DecoupledEditor - .create( editorData, { - plugins: [ Paragraph, Bold, DataInitAssertPlugin ] - } ) - .then( newEditor => { - sinon.assert.calledOnce( dataInitSpy ); + return CustomDecoupledEditor + .create( getElementOrData(), { + plugins: [ Paragraph, Bold ] + } ) + .then( newEditor => { + expect( newEditor ).to.be.instanceof( CustomDecoupledEditor ); + expect( newEditor ).to.be.instanceof( DecoupledEditor ); - return newEditor.destroy(); - } ); - } ); + expect( newEditor.getData() ).to.equal( '

foo bar

' ); - describe( 'ui', () => { - it( 'attaches editable UI as view\'s DOM root', () => { - expect( editor.editing.view.getDomRoot() ).to.equal( editor.ui.view.editable.element ); + return newEditor.destroy(); + } ); } ); - } ); - } ); - describe( 'create - events', () => { - afterEach( () => { - return editor.destroy(); - } ); + // https://github.com/ckeditor/ckeditor5-editor-decoupled/issues/3 + it( 'initializes the data controller', () => { + let dataInitSpy; - it( 'fires all events in the right order', () => { - const fired = []; + class DataInitAssertPlugin extends Plugin { + constructor( editor ) { + super(); - function spy( evt ) { - fired.push( evt.name ); - } + this.editor = editor; + } - class EventWatcher extends Plugin { - init() { - this.editor.on( 'pluginsReady', spy ); - this.editor.on( 'uiReady', spy ); - this.editor.on( 'dataReady', spy ); - this.editor.on( 'ready', spy ); + init() { + dataInitSpy = sinon.spy( this.editor.data, 'init' ); + } } - } - return DecoupledEditor - .create( editorData, { - plugins: [ EventWatcher ] - } ) - .then( newEditor => { - expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] ); + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ Paragraph, Bold, DataInitAssertPlugin ] + } ) + .then( newEditor => { + sinon.assert.calledOnce( dataInitSpy ); + + return newEditor.destroy(); + } ); + } ); - editor = newEditor; + describe( 'events', () => { + it( 'fires all events in the right order', () => { + const fired = []; + + function spy( evt ) { + fired.push( evt.name ); + } + + class EventWatcher extends Plugin { + init() { + this.editor.on( 'pluginsReady', spy ); + this.editor.on( 'uiReady', spy ); + this.editor.on( 'dataReady', spy ); + this.editor.on( 'ready', spy ); + } + } + + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ EventWatcher ] + } ) + .then( newEditor => { + expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] ); + + return newEditor.destroy(); + } ); + } ); + + it( 'fires dataReady once data is loaded', () => { + let data; + + class EventWatcher extends Plugin { + init() { + this.editor.on( 'dataReady', () => { + data = this.editor.getData(); + } ); + } + } + + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ EventWatcher, Paragraph, Bold ] + } ) + .then( newEditor => { + expect( data ).to.equal( '

foo bar

' ); + + return newEditor.destroy(); + } ); } ); - } ); - it( 'fires dataReady once data is loaded', () => { - let data; + it( 'fires uiReady once UI is rendered', () => { + let isReady; + + class EventWatcher extends Plugin { + init() { + this.editor.on( 'uiReady', () => { + isReady = this.editor.ui.view.isRendered; + } ); + } + } + + return DecoupledEditor + .create( getElementOrData(), { + plugins: [ EventWatcher ] + } ) + .then( newEditor => { + expect( isReady ).to.be.true; + + return newEditor.destroy(); + } ); + } ); + } ); + } + } ); - class EventWatcher extends Plugin { - init() { - this.editor.on( 'dataReady', () => { - data = this.editor.getData(); + describe( 'destroy', () => { + describe( 'editor with data', () => { + beforeEach( function() { + return DecoupledEditor + .create( editorData, { plugins: [ Paragraph ] } ) + .then( newEditor => { + editor = newEditor; } ); - } - } - - return DecoupledEditor - .create( editorData, { - plugins: [ EventWatcher, Paragraph, Bold ] - } ) - .then( newEditor => { - expect( data ).to.equal( '

foo bar

' ); + } ); - editor = newEditor; - } ); + test( () => editorData ); } ); - it( 'fires uiReady once UI is rendered', () => { - let isReady; + describe( 'editor with editable element', () => { + let editableElement; + + beforeEach( function() { + editableElement = document.createElement( 'div' ); + editableElement.innerHTML = editorData; - class EventWatcher extends Plugin { - init() { - this.editor.on( 'uiReady', () => { - isReady = this.editor.ui.view.isRendered; + return DecoupledEditor + .create( editableElement, { plugins: [ Paragraph ] } ) + .then( newEditor => { + editor = newEditor; } ); - } - } + } ); - return DecoupledEditor - .create( editorData, { - plugins: [ EventWatcher ] - } ) - .then( newEditor => { - expect( isReady ).to.be.true; + it( 'sets data back to the element', () => { + editor.setData( '

foo

' ); - editor = newEditor; - } ); - } ); - } ); + return editor.destroy() + .then( () => { + expect( editableElement.innerHTML ).to.equal( '

foo

' ); + } ); + } ); - describe( 'destroy', () => { - beforeEach( function() { - return DecoupledEditor - .create( editorData, { plugins: [ Paragraph ] } ) - .then( newEditor => { - editor = newEditor; - } ); + test( () => editableElement ); } ); - it( 'destroys the UI', () => { - const spy = sinon.spy( editor.ui, 'destroy' ); + function test() { + it( 'destroys the UI', () => { + const spy = sinon.spy( editor.ui, 'destroy' ); - return editor.destroy() - .then( () => { - sinon.assert.calledOnce( spy ); - } ); - } ); + return editor.destroy() + .then( () => { + sinon.assert.calledOnce( spy ); + } ); + } ); + } } ); } ); diff --git a/tests/decouplededitorui.js b/tests/decouplededitorui.js index 907a2fc..943477b 100644 --- a/tests/decouplededitorui.js +++ b/tests/decouplededitorui.js @@ -63,45 +63,6 @@ describe( 'DecoupledEditorUI', () => { expect( view.isRendered ).to.be.true; } ); - describe( 'config', () => { - it( 'does nothing if not specified', () => { - expect( view.toolbar.element.parentElement ).to.be.null; - expect( view.editable.element.parentElement ).to.be.null; - } ); - - it( 'allocates view#toolbar', () => { - return VirtualDecoupledTestEditor - .create( { - toolbar: [ 'foo', 'bar' ], - toolbarContainer: document.body - } ) - .then( newEditor => { - expect( newEditor.ui.view.toolbar.element.parentElement ).to.equal( document.body ); - - return newEditor; - } ) - .then( newEditor => { - newEditor.destroy(); - } ); - } ); - - it( 'allocates view#editable', () => { - return VirtualDecoupledTestEditor - .create( { - toolbar: [ 'foo', 'bar' ], - editableContainer: document.body - } ) - .then( newEditor => { - expect( newEditor.ui.view.editable.element.parentElement ).to.equal( document.body ); - - return newEditor; - } ) - .then( newEditor => { - newEditor.destroy(); - } ); - } ); - } ); - describe( 'editable', () => { it( 'registers view.editable#element in editor focus tracker', () => { ui.focusTracker.isFocused = false; @@ -139,6 +100,10 @@ describe( 'DecoupledEditorUI', () => { { isReadOnly: true } ); } ); + + it( 'attaches editable UI as view\'s DOM root', () => { + expect( editor.editing.view.getDomRoot() ).to.equal( view.editable.element ); + } ); } ); describe( 'view.toolbar#items', () => { @@ -207,43 +172,7 @@ describe( 'DecoupledEditorUI', () => { return editor.destroy() .then( () => { sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, false, false ); - } ); - } ); - - it( 'removes view#toolbar from DOM, if config.toolbarContainer is specified', () => { - let spy; - - return VirtualDecoupledTestEditor - .create( { - toolbar: [ 'foo', 'bar' ], - toolbarContainer: document.body - } ) - .then( newEditor => { - spy = sinon.spy( newEditor.ui.view, 'destroy' ); - - newEditor.destroy(); - } ) - .then( () => { - sinon.assert.calledWithExactly( spy, true, false ); - } ); - } ); - - it( 'removes view#editable from DOM, if config.editableContainer is specified', () => { - let spy; - - return VirtualDecoupledTestEditor - .create( { - toolbar: [ 'foo', 'bar' ], - editableContainer: document.body - } ) - .then( newEditor => { - spy = sinon.spy( newEditor.ui.view, 'destroy' ); - - newEditor.destroy(); - } ) - .then( () => { - sinon.assert.calledWithExactly( spy, false, true ); + sinon.assert.calledWithExactly( spy ); } ); } ); } ); diff --git a/tests/decouplededitoruiview.js b/tests/decouplededitoruiview.js index bfbcf07..68b234d 100644 --- a/tests/decouplededitoruiview.js +++ b/tests/decouplededitoruiview.js @@ -43,8 +43,9 @@ describe( 'DecoupledEditorUIView', () => { expect( view.toolbar.element.parentElement ).to.be.null; } ); - it( 'gets the .ck-reset_all class', () => { + it( 'gets the CSS classes', () => { expect( view.toolbar.element.classList.contains( 'ck-reset_all' ) ).to.be.true; + expect( view.toolbar.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true; } ); } ); @@ -53,7 +54,7 @@ describe( 'DecoupledEditorUIView', () => { expect( view.editable ).to.be.instanceof( InlineEditableUIView ); } ); - it( 'is given a locate object', () => { + it( 'is given a locale object', () => { expect( view.editable.locale ).to.equal( locale ); } ); @@ -61,6 +62,17 @@ describe( 'DecoupledEditorUIView', () => { expect( view.isRendered ).to.be.true; expect( view.editable.element.parentElement ).to.be.null; } ); + + it( 'can be created out of an existing DOM element', () => { + const editableElement = document.createElement( 'div' ); + const testView = new DecoupledEditorUIView( locale, editableElement ); + + testView.render(); + + expect( testView.editable.element ).to.equal( editableElement ); + + testView.destroy(); + } ); } ); } ); @@ -87,30 +99,6 @@ describe( 'DecoupledEditorUIView', () => { view.toolbar.element.remove(); view.editable.element.remove(); } ); - - it( 'removes toolbar#element on demand', () => { - document.body.appendChild( view.toolbar.element ); - document.body.appendChild( view.editable.element ); - - view.destroy( true ); - - expect( view.toolbar.element.parentElement ).to.be.null; - expect( view.editable.element.parentElement ).to.equal( document.body ); - - view.editable.element.remove(); - } ); - - it( 'removes editable#element on demand', () => { - document.body.appendChild( view.toolbar.element ); - document.body.appendChild( view.editable.element ); - - view.destroy( false, true ); - - expect( view.toolbar.element.parentElement ).to.equal( document.body ); - expect( view.editable.element.parentElement ).to.be.null; - - view.toolbar.element.remove(); - } ); } ); describe( 'editableElement', () => { diff --git a/tests/manual/decouplededitor-editable.html b/tests/manual/decouplededitor-editable.html new file mode 100644 index 0000000..25498f3 --- /dev/null +++ b/tests/manual/decouplededitor-editable.html @@ -0,0 +1,58 @@ +

+ + +

+ +

The toolbar

+
+ +

The editable

+
+
+

This element becomes the editable

+

It has the initial editor data. It should keep it after the editor is destroyed too.

+
+
+ + diff --git a/tests/manual/decouplededitor-editable.js b/tests/manual/decouplededitor-editable.js new file mode 100644 index 0000000..982ed33 --- /dev/null +++ b/tests/manual/decouplededitor-editable.js @@ -0,0 +1,57 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console:false, document, window */ + +import DecoupledEditor from '../../src/decouplededitor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import testUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +let editor, editable, observer; + +function initEditor() { + DecoupledEditor + .create( document.querySelector( '.editor__editable' ), { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ] + } ) + .then( newEditor => { + console.log( 'Editor was initialized', newEditor ); + console.log( 'You can now play with it using global `editor` and `editable` variables.' ); + + document.querySelector( '.toolbar-container' ).appendChild( newEditor.ui.view.toolbar.element ); + + window.editor = editor = newEditor; + window.editable = editable = editor.editing.view.document.getRoot(); + + observer = testUtils.createObserver(); + observer.observe( 'Editable', editable, [ 'isFocused' ] ); + } ) + .catch( err => { + console.error( err.stack ); + } ); +} + +function destroyEditor() { + editor.destroy() + .then( () => { + window.editor = editor = null; + window.editable = editable = null; + + observer.stopListening(); + observer = null; + + console.log( 'Editor was destroyed' ); + } ); +} + +document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor ); +document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor ); diff --git a/tests/manual/decouplededitor-editable.md b/tests/manual/decouplededitor-editable.md new file mode 100644 index 0000000..de48bb6 --- /dev/null +++ b/tests/manual/decouplededitor-editable.md @@ -0,0 +1,20 @@ +1. Click "Init editor". +2. Expected: + * The toolbar container should get the toolbar. + * The toolbar should appear with "Heading", "Bold", "Italic", "Undo" and "Redo" buttons. + * **The yellow element should become an editable**. +3. Do some editing and formatting. +4. Click "Destroy editor". +5. Expected: + * Editor should be destroyed. + * The toolbar should disappear from the container. + * **The editable must remain**. + * **The editable must retain the editor data**. + * The `.ck-body` region should be removed. + +## Notes: + +* You can play with: + * `editable.isReadOnly`, +* Changes to `editable.isFocused` should be logged to the console. +* Features should work. diff --git a/tests/manual/decouplededitor.js b/tests/manual/decouplededitor.js index d3dfff6..3c5d329 100644 --- a/tests/manual/decouplededitor.js +++ b/tests/manual/decouplededitor.js @@ -22,15 +22,15 @@ function initEditor() { DecoupledEditor .create( editorData, { plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], - - toolbarContainer: document.querySelector( '.toolbar-container' ), - editableContainer: document.querySelector( '.editable-container' ) + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ] } ) .then( newEditor => { console.log( 'Editor was initialized', newEditor ); console.log( 'You can now play with it using global `editor` and `editable` variables.' ); + document.querySelector( '.toolbar-container' ).appendChild( newEditor.ui.view.toolbar.element ); + document.querySelector( '.editable-container' ).appendChild( newEditor.ui.view.editable.element ); + window.editor = editor = newEditor; window.editable = editable = editor.editing.view.document.getRoot(); diff --git a/tests/manual/decouplededitor.md b/tests/manual/decouplededitor.md index 5085f73..06f5ce5 100644 --- a/tests/manual/decouplededitor.md +++ b/tests/manual/decouplededitor.md @@ -5,8 +5,8 @@ 3. Click "Destroy editor". 4. Expected: * Editor should be destroyed. - * The editor UI should disappear from the containers. - * The 'ck-body region' should be removed. + * **The editor UI should remain in the containers**. + * The `.ck-body` region should be removed. ## Notes: