Browse Source

Merge pull request #1333 from ckeditor/t/ckeditor5-core/120

Feature: Introduced decorable DataController#init metohd. Closes ckeditor/ckeditor5-core#120.
Piotr Jasiun 7 years ago
parent
commit
dd9c0c109d

+ 43 - 2
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -9,6 +9,7 @@
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import Mapper from '../conversion/mapper';
 
@@ -100,6 +101,8 @@ export default class DataController {
 		this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
 		this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+		this.decorate( 'init' );
 	}
 
 	/**
@@ -171,8 +174,41 @@ export default class DataController {
 	}
 
 	/**
+	 * Sets initial input data parsed by the {@link #processor data processor} and
+	 * converted by the {@link #upcastDispatcher view-to-model converters}.
+	 * Initial data can be set only to document that {@link module:engine/model/document~Document#version} is equal 0.
+	 *
+	 * **Note** This method is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} which is
+	 * used by e.g. collaborative editing plugin that syncs remote data on init.
+	 *
+	 * @fires init
+	 * @param {String} data Input data.
+	 * @param {String} [rootName='main'] Root name.
+	 */
+	init( data, rootName = 'main' ) {
+		if ( this.model.document.version ) {
+			/**
+			 * Cannot set initial data to not empty {@link module:engine/model/document~Document}.
+			 * Initial data should be set once, during {@link module:core/editor/editor~Editor} initialization,
+			 * when the {@link module:engine/model/document~Document#version} is equal 0.
+			 *
+			 * @error datacontroller-init-document-not-empty
+			 */
+			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
+		}
+
+		const modelRoot = this.model.document.getRoot( rootName );
+
+		this.model.enqueueChange( 'transparent', writer => {
+			writer.insert( this.parse( data, modelRoot ), modelRoot );
+		} );
+	}
+
+	/**
 	 * Sets input data parsed by the {@link #processor data processor} and
 	 * converted by the {@link #upcastDispatcher view-to-model converters}.
+	 * This method can be used any time to replace existing editor data by the new one without clearing the
+	 * {@link module:engine/model/document~Document#history document history}.
 	 *
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
@@ -185,8 +221,6 @@ export default class DataController {
 		const modelRoot = this.model.document.getRoot( rootName );
 
 		this.model.enqueueChange( 'transparent', writer => {
-			// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
-			// After fixing it this code should be removed.
 			writer.setSelection( null );
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
@@ -237,6 +271,13 @@ export default class DataController {
 	 * Removes all event listeners set by the DataController.
 	 */
 	destroy() {}
+
+	/**
+	 * Event fired by decorated {@link #init} method.
+	 * See {@link module:utils/observablemixin~ObservableMixin.decorate} for more information and samples.
+	 *
+	 * @event init
+	 */
 }
 
 mix( DataController, ObservableMixin );

+ 56 - 0
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -7,6 +7,7 @@ import Model from '../../src/model/model';
 import Range from '../../src/model/range';
 import DataController from '../../src/controller/datacontroller';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ViewDocumentFragment from '../../src/view/documentfragment';
@@ -152,6 +153,61 @@ describe( 'DataController', () => {
 		} );
 	} );
 
+	describe( 'init()', () => {
+		it( 'should be decorated', () => {
+			const spy = sinon.spy();
+
+			data.on( 'init', spy );
+
+			data.init( 'foo bar' );
+
+			sinon.assert.calledWithExactly( spy, sinon.match.any, [ 'foo bar' ] );
+		} );
+
+		it( 'should throw an error when document data is already initialized', () => {
+			data.init( '<p>Foo</p>' );
+
+			expect( () => {
+				data.init( '<p>Bar</p>' );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.'
+			);
+		} );
+
+		it( 'should set data to default main root', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+			data.init( 'foo' );
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
+		} );
+
+		it( 'should get root name as a parameter', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+			data.init( 'foo', 'title' );
+
+			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'foo' );
+		} );
+
+		it( 'should create a batch', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+			data.init( 'foo' );
+
+			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 1 );
+		} );
+
+		it( 'should cause firing change event', () => {
+			const spy = sinon.spy();
+
+			schema.extend( '$text', { allowIn: '$root' } );
+			model.document.on( 'change', spy );
+
+			data.init( 'foo' );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
 	describe( 'set()', () => {
 		it( 'should set data to default main root', () => {
 			schema.extend( '$text', { allowIn: '$root' } );