Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Allow to pass initial data to the editor constructor #73

Merged
merged 16 commits into from
Jul 3, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions src/classiceditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ClassicEditorUIView from './classiceditoruiview';
import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement';

/**
* The {@glink builds/guides/overview#classic-editor classic editor} implementation.
Expand Down Expand Up @@ -53,23 +54,26 @@ export default class ClassicEditor extends Editor {
* {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`} method instead.
*
* @protected
* @param {HTMLElement} element The DOM element that will be the source for the created editor.
* The data will be loaded from it and loaded back to it once the editor is destroyed.
* @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
* or editor's initial data. For more information see
* {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`}.
* @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
*/
constructor( element, config ) {
constructor( sourceElementOrData, config ) {
super( config );

if ( isElement( sourceElementOrData ) ) {
this.sourceElement = sourceElementOrData;
}

/**
* The element replacer instance used to hide the editor element.
* The element replacer instance used to hide the editor source element.
*
* @protected
* @member {module:utils/elementreplacer~ElementReplacer}
*/
this._elementReplacer = new ElementReplacer();

this.element = element;

this.data.processor = new HtmlDataProcessor();

this.model.document.createRoot();
Expand All @@ -79,15 +83,25 @@ export default class ClassicEditor extends Editor {
attachToForm( this );
}

/**
* @inheritDoc
*/
get element() {
return this.ui.view.element;
}

/**
* Destroys the editor instance, releasing all resources used by it.
*
* Updates the original editor element with the data.
* Updates the source editor element with the data.
*
* @returns {Promise}
*/
destroy() {
this.updateElement();
if ( this.sourceElement ) {
this.updateSourceElement();
}

this._elementReplacer.restore();
this.ui.destroy();

Expand Down Expand Up @@ -128,25 +142,69 @@ export default class ClassicEditor extends Editor {
* console.error( err.stack );
* } );
*
* @param {HTMLElement} element The DOM element that will be the source for the created editor.
* The data will be loaded from it and loaded back to it once the editor is destroyed.
* Creating instance when using initial data instead of a DOM element:
*
* import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
* import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
* import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
* import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
* import ...
*
* ClassicEditor
* .create( '<p>Hello world!</p>', {
* plugins: [ Essentials, Bold, Italic, ... ],
* toolbar: [ 'bold', 'italic', ... ]
* } )
* .then( editor => {
* console.log( 'Editor was initialized', editor );
*
* // Initial data was provided so `editor.element` needs to be added manually to the DOM.
* document.body.appendChild( editor.element );
* } )
* .catch( err => {
* console.error( err.stack );
* } );
*
* @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
* or editor's initial data.
*
* If an element is passed, then its contents will be automatically
* {@link module:editor-classic/classiceditor~ClassicEditor#setData loaded} to the editor on startup
* and the {@link module:core/editor/editorwithui~EditorWithUI#element editor element} will replace the passed element in the DOM
* (the original one will be hidden and editor will be injected next to it).
*
* Moreover, the data will be set back to the source element once the editor is destroyed and
* (if the element is a `<textarea>`) when a form in which this element is contained is submitted (which ensures
* automatic integration with native web forms).
*
* If a data is passed, a detached editor will be created. It means that you need to insert it into the DOM manually
* (by accessing the {@link module:editor-classic/classiceditor~ClassicEditor#element `editor.element`} property).
*
* See the examples above to learn more.
* @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-classic/classiceditor~ClassicEditor} instance.
*/
static create( element, config ) {
static create( sourceElementOrData, config ) {
return new Promise( resolve => {
const editor = new this( element, config );
const editor = new this( sourceElementOrData, config );

resolve(
editor.initPlugins()
.then( () => editor.ui.init() )
.then( () => {
editor._elementReplacer.replace( element, editor.ui.view.element );
if ( isElement( sourceElementOrData ) ) {
editor._elementReplacer.replace( sourceElementOrData, editor.element );
}

editor.fire( 'uiReady' );
} )
.then( () => editor.editing.view.attachDomRoot( editor.ui.view.editableElement ) )
.then( () => editor.data.init( getDataFromElement( element ) ) )
.then( () => {
Copy link
Member

@Reinmar Reinmar Jun 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be easier to read if it was written like this:

const initialData = isElement(  sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData;

editor.data.init( initialData );

Copy link
Member

@Reinmar Reinmar Jun 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is – what differs these two scenarios isn't how or whether you call editor.data.init(). It's how you retrieve this data and from where.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the code you committed isn't equal to what I proposed:

editor.data.init(
    isElement( sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData
);

In this case, we lose the information what do we call init() with. It makes reading the code harder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, editor.data.init() may return a promise so this change breaks the promise chain – make sure to add a test for that.

editor.data.init(
isElement( sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData
);
} )
.then( () => {
editor.fire( 'dataReady' );
editor.fire( 'ready' );
Expand Down
63 changes: 59 additions & 4 deletions tests/classiceditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,21 @@ describe( 'ClassicEditor', () => {
} );

it( 'has a Data Interface', () => {
testUtils.isMixed( ClassicEditor, DataApiMixin );
expect( testUtils.isMixed( ClassicEditor, DataApiMixin ) ).to.true;
} );

it( 'has a Element Interface', () => {
testUtils.isMixed( ClassicEditor, ElementApiMixin );
expect( testUtils.isMixed( ClassicEditor, ElementApiMixin ) ).to.true;
} );

it( 'creates main root element', () => {
expect( editor.model.document.getRoot( 'main' ) ).to.instanceof( RootElement );
} );

it( 'contains the source element as #sourceElement property', () => {
expect( editor.sourceElement ).to.equal( editorElement );
} );

it( 'handles form element', () => {
const form = document.createElement( 'form' );
const textarea = document.createElement( 'textarea' );
Expand Down Expand Up @@ -88,6 +92,14 @@ describe( 'ClassicEditor', () => {
} );
} );

it( 'allows to pass data to the constructor', () => {
return ClassicEditor.create( '<p>Hello world!</p>', {
plugins: [ Paragraph ]
} ).then( editor => {
expect( editor.getData() ).to.equal( '<p>Hello world!</p>' );
} );
} );

describe( 'ui', () => {
it( 'creates the UI using BoxedEditorUI classes', () => {
expect( editor.ui ).to.be.instanceof( ClassicEditorUI );
Expand Down Expand Up @@ -137,6 +149,18 @@ describe( 'ClassicEditor', () => {
} );
} );

it( 'should have undefined the #sourceElement if editor was initialized with data', () => {
return ClassicEditor
.create( '<p>Foo.</p>', {
plugins: [ Paragraph, Bold ]
} )
.then( newEditor => {
expect( newEditor.sourceElement ).to.be.undefined;

return newEditor.destroy();
} );
} );

describe( 'ui', () => {
it( 'inserts editor UI next to editor element', () => {
expect( editor.ui.view.element.previousSibling ).to.equal( editorElement );
Expand All @@ -145,6 +169,20 @@ describe( 'ClassicEditor', () => {
it( 'attaches editable UI as view\'s DOM root', () => {
expect( editor.editing.view.getDomRoot() ).to.equal( editor.ui.view.editable.element );
} );

it( 'editor.element points to the editor\'s UI when editor was initialized on the DOM element', () => {
expect( editor.element ).to.equal( editor.ui.view.element );
} );

it( 'editor.element points to the editor\'s UI when editor was initialized with data', () => {
return ClassicEditor.create( '<p>Hello world!</p>', {
plugins: [ Paragraph ]
} ).then( editor => {
expect( editor.element ).to.equal( editor.ui.view.element );

return editor.destroy();
} );
} );
} );
} );

Expand Down Expand Up @@ -243,12 +281,29 @@ describe( 'ClassicEditor', () => {
} );
} );

it( 'does not update the source element if editor was initialized with data', () => {
return ClassicEditor
.create( '<p>Foo.</p>', {
plugins: [ Paragraph, Bold ]
} )
.then( newEditor => {
const spy = sinon.stub( newEditor, 'updateSourceElement' );

return newEditor.destroy()
.then( () => {
expect( spy.called ).to.be.false;

spy.restore();
} );
} );
} );

it( 'restores the editor element', () => {
expect( editor.element.style.display ).to.equal( 'none' );
expect( editor.sourceElement.style.display ).to.equal( 'none' );

return editor.destroy()
.then( () => {
expect( editor.element.style.display ).to.equal( '' );
expect( editor.sourceElement.style.display ).to.equal( '' );
} );
} );
} );
Expand Down
18 changes: 18 additions & 0 deletions tests/manual/classiceditor-data.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<p>
<button id="destroyEditors">Destroy editors</button>
<button id="initEditor">Init editor</button>
</p>

<div class="container"></div>

<style>
body {
width: 10000px;
height: 10000px;
}

.container {
padding: 20px;
width: 500px;
}
</style>
50 changes: 50 additions & 0 deletions tests/manual/classiceditor-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals console:false, document, window */

import ClassicEditor from '../../src/classiceditor';
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';

window.editors = [];
let counter = 1;

const container = document.querySelector( '.container' );

function initEditor() {
ClassicEditor
.create( `<h2>Hello world! #${ counter }</h2><p>This is an editor instance.</p>`, {
plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ],
toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
counter += 1;
window.editors.push( editor );
container.appendChild( editor.element );
} )
.catch( err => {
console.error( err.stack );
} );
}

function destroyEditors() {
window.editors.forEach( editor => {
editor.destroy()
.then( () => {
editor.element.remove();
} );
} );
window.editors = [];
counter = 1;
}

document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );
document.getElementById( 'destroyEditors' ).addEventListener( 'click', destroyEditors );
3 changes: 3 additions & 0 deletions tests/manual/classiceditor-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1. Click "Init editor".
2. New editor instance should be appended to the document with initial data in it. You can create more than one editor.
3. After clicking "Destroy editor" all editors should be removed from the document.
4 changes: 2 additions & 2 deletions tests/manual/classiceditor.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
1. Click "Init editor".
2. Expected:
* Framed editor should be created.
* Original element should disappear.
* Source element should disappear.
* There should be a toolbar with "Bold", "Italic", "Undo" and "Redo" buttons.
3. Click "Destroy editor".
4. Expected:
* Editor should be destroyed.
* Original element should be visible.
* Source element should be visible.
* The element should contain its data (updated).
* The 'ck-body region' should be removed.

Expand Down