From 9170a5bf0667ecc3bd3c868e160043760e11f2bd Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 27 Mar 2018 16:02:11 +0200 Subject: [PATCH] Allowed the editable element to be passed into DecoupledEditor.constructor(). Removed config.toolbarContainer and config.editableContainer. --- docs/framework/guides/document-editor.md | 31 +- src/decouplededitor.js | 148 +++++----- src/decouplededitorui.js | 27 +- src/decouplededitoruiview.js | 29 +- tests/decouplededitor.js | 318 ++++++++++++--------- tests/decouplededitorui.js | 81 +----- tests/decouplededitoruiview.js | 37 +-- tests/manual/decouplededitor-editable.html | 58 ++++ tests/manual/decouplededitor-editable.js | 57 ++++ tests/manual/decouplededitor-editable.md | 20 ++ tests/manual/decouplededitor.js | 8 +- tests/manual/decouplededitor.md | 4 +- 12 files changed, 439 insertions(+), 379 deletions(-) create mode 100644 tests/manual/decouplededitor-editable.html create mode 100644 tests/manual/decouplededitor-editable.js create mode 100644 tests/manual/decouplededitor-editable.md diff --git a/docs/framework/guides/document-editor.md b/docs/framework/guides/document-editor.md index 910317d..3e79e01 100644 --- a/docs/framework/guides/document-editor.md +++ b/docs/framework/guides/document-editor.md @@ -19,21 +19,26 @@ The `DecoupledDocumentEditor` includes all the necessary features for the task. 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 DOM. Instead, it accepts the string containing the initial data as a 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 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 don't specify these configuration options, then 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 `uiReady`} event. The toolbar element is accessible via `editor.ui.view.toolbar.element`. The document editor supports the Easy Image 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. @@ -58,9 +61,13 @@ The code we just created will run the editor but still, the user interface is mi The following structure has two containers which correspond to the configuration we have just used. The editor will inject the toolbar and editable into respective containers as it starts. ```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 app. */ -.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 114f220..a46bdee 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. @@ -54,28 +57,67 @@ 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 ); + } + } ); } /** @@ -84,14 +126,11 @@ export default class DecoupledEditor extends Editor { * Creating instance when using the {@glink builds/index CKEditor build}: * * DecoupledEditor - * .create( '

Editor data

', { - * // The location of the toolbar in DOM. - * toolbarContainer: document.querySelector( 'body div.toolbar-container' ), - * - * // The location of the editable in 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 DOM. - * toolbarContainer: document.querySelector( 'div.toolbar-container' ), - * - * // The location of the editable in 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**: {@link module:core/editor/editorconfig~EditorConfig#toolbarContainer `config.toolbarContainer`} and - * {@link module:core/editor/editorconfig~EditorConfig#editableContainer `config.editableContainer`} 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 the pure data string. The editor will then render + * an editable element that must be inserted into 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 ); - -/** - * A 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 DOM. See - * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`} - * to learn more. - * - * @member {HTMLElement} module:core/editor/editorconfig~EditorConfig#toolbarContainer - */ - -/** - * A 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 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 cf5b4c8..a36ebfd 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' ) ); - - /** - * A container of the {@link module:editor-decoupled/decouplededitoruiview~DecoupledEditorUIView#toolbar}. - * - * @type {HTMLElement|String} - * @private - */ - this._toolbarContainer = editor.config.get( 'toolbarContainer' ); - - /** - * A container of 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 2c7fe8c..6ecff08 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 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 a 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,7 +47,7 @@ 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 needs to be reset in it. Template.extend( this.toolbar.template, { @@ -59,25 +59,6 @@ export default class DecoupledEditorUIView extends EditorUIView { this.registerChildren( [ this.toolbar, this.editable ] ); } - /** - * Destroys the view and removes {@link #toolbar} and {@link #editable} - * {@link module:ui/view~View#element `element`} from DOM, if required. - * - * @param {Boolean} [removeToolbar] When `true`, remove the {@link #toolbar} element from DOM. - * @param {Boolean} [removeEditable] When `true`, remove the {@link #editable} element from 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..95ae304 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,220 @@ describe( 'DecoupledEditor', () => { } ); describe( 'create()', () => { - beforeEach( () => { - return DecoupledEditor - .create( editorData, { - plugins: [ Paragraph, Bold ] - } ) - .then( newEditor => { - editor = newEditor; - } ); - } ); - afterEach( () => { return editor.destroy(); } ); - it( 'creates an instance which inherits from the DecoupledEditor', () => { - expect( editor ).to.be.instanceof( DecoupledEditor ); - } ); + describe( 'editor with data', () => { + beforeEach( () => { + return DecoupledEditor + .create( editorData, { + plugins: [ Paragraph, Bold ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); - it( 'loads the initial data', () => { - expect( editor.getData() ).to.equal( '

foo bar

' ); + test( () => editorData ); } ); - // #53 - it( 'creates an instance of a DecoupledEditor child class', () => { - class CustomDecoupledEditor extends DecoupledEditor {} + describe( 'editor with editable element', () => { + let editableElement; - return CustomDecoupledEditor - .create( editorData, { - plugins: [ Paragraph, Bold ] - } ) - .then( newEditor => { - expect( newEditor ).to.be.instanceof( CustomDecoupledEditor ); - expect( newEditor ).to.be.instanceof( DecoupledEditor ); + beforeEach( () => { + editableElement = document.createElement( 'div' ); + editableElement.innerHTML = editorData; - expect( newEditor.getData() ).to.equal( '

foo bar

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

foo bar

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

foo bar

' ); - return newEditor.destroy(); - } ); - } ); - - 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; + 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(); + } ); + } ); - class EventWatcher extends Plugin { - init() { - this.editor.on( 'dataReady', () => { - data = this.editor.getData(); - } ); - } - } + 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(); + } ); + } ); + } ); + } + } ); - return DecoupledEditor - .create( editorData, { - plugins: [ EventWatcher, Paragraph, Bold ] - } ) - .then( newEditor => { - expect( data ).to.equal( '

foo bar

' ); + describe( 'destroy', () => { + describe( 'editor with data', () => { + beforeEach( function() { + return DecoupledEditor + .create( editorData, { plugins: [ Paragraph ] } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); - 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..f5a4590 100644 --- a/tests/decouplededitoruiview.js +++ b/tests/decouplededitoruiview.js @@ -53,7 +53,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 +61,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 +98,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: