Browse Source

Merge pull request #73 from ckeditor/t/72

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.
Piotrek Koszuliński 7 years ago
parent
commit
a5215aed9c

+ 75 - 14
packages/ckeditor5-editor-classic/src/classiceditor.js

@@ -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.
@@ -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();
@@ -80,14 +84,24 @@ export default class ClassicEditor extends Editor {
 	}
 
 	/**
+	 * @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();
 
@@ -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' );

+ 59 - 4
packages/ckeditor5-editor-classic/tests/classiceditor.js

@@ -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' );
@@ -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 );
@@ -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 );
@@ -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();
+				} );
+			} );
 		} );
 	} );
 
@@ -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( '' );
 				} );
 		} );
 	} );

+ 18 - 0
packages/ckeditor5-editor-classic/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 - 0
packages/ckeditor5-editor-classic/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 - 0
packages/ckeditor5-editor-classic/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.

+ 2 - 2
packages/ckeditor5-editor-classic/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.