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

Commit

Permalink
Merge pull request #73 from ckeditor/t/72
Browse files Browse the repository at this point in the history
Feature: Editor can now be created with initial data passed to the `create()` method. Closes #72.

BREAKING CHANGE: The `ClassicEditor#element` property was renamed to `ClassicEditor#sourceElement`. See ckeditor/ckeditor5-core#64.
  • Loading branch information
Reinmar committed Jul 3, 2018
2 parents ae98cfd + 6df6ba9 commit 09cebc6
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 20 deletions.
89 changes: 75 additions & 14 deletions src/classiceditor.js
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's 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 editor's source 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,72 @@ 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 a source 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( () => {
const initialData = isElement( sourceElementOrData ) ?
getDataFromElement( sourceElementOrData ) :
sourceElementOrData;

return editor.data.init( initialData );
} )
.then( () => {
editor.fire( 'dataReady' );
editor.fire( 'ready' );
Expand Down
63 changes: 59 additions & 4 deletions tests/classiceditor.js
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
@@ -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
@@ -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
@@ -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
@@ -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

0 comments on commit 09cebc6

Please sign in to comment.