瀏覽代碼

Merge pull request #1627 from ckeditor/t/1626

Other: Added support for handling data in multiple roots in `DataController`. Closes #1626.

BREAKING CHANGE: The second parameter (`rootName`) from `DataController#init()` method has been removed. To initialize data on a root different than default one an object with `rootName` - `data` pair should be passed.

BREAKING CHANGE: The second parameter (`rootName`) from `DataController#set()` method has been removed. To set data on a root different than default one an object with `rootName` - `data` pair should be passed.
Piotr Jasiun 7 年之前
父節點
當前提交
ecc74cffe8

+ 107 - 12
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -113,6 +113,21 @@ export default class DataController {
 	 * @returns {String} Output data.
 	 */
 	get( rootName = 'main' ) {
+		if ( !this._checkIfRootsExists( [ rootName ] ) ) {
+			/**
+			 * Cannot get data from a non-existing root. This error is thrown when {@link #get DataController#get() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #get} like:
+			 *
+			 * 		data.get( 'root2' );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-get-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
+		}
+
 		// Get model range.
 		return this.stringify( this.model.document.getRoot( rootName ) );
 	}
@@ -181,12 +196,20 @@ export default class DataController {
 	 * **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.
 	 *
+	 * When data is passed as a string it is initialized on a default `main` root:
+	 *
+	 *		dataController.init( '<p>Foo</p>' ); // Initializes data on the `main` root.
+	 *
+	 * To initialize data on a different root or multiple roots at once, object containing `rootName` - `data` pairs should be passed:
+	 *
+	 *		dataController.init( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Initializes data on the `main` and `title` roots.
+	 *
 	 * @fires init
-	 * @param {String} data Input data.
-	 * @param {String} [rootName='main'] Root name.
+	 * @param {String|Object.<String,String>} data Input data as a string or an object containing `rootName` - `data`
+	 * pairs to initialize data on multiple roots at once.
 	 * @returns {Promise} Promise that is resolved after the data is set on the editor.
 	 */
-	init( data, rootName = 'main' ) {
+	init( data ) {
 		if ( this.model.document.version ) {
 			/**
 			 * Cannot set initial data to not empty {@link module:engine/model/document~Document}.
@@ -198,10 +221,33 @@ export default class DataController {
 			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
 		}
 
-		const modelRoot = this.model.document.getRoot( rootName );
+		let initialData = {};
+		if ( typeof data === 'string' ) {
+			initialData.main = data; // Default root is 'main'. To initiate data on a different root, object should be passed.
+		} else {
+			initialData = data;
+		}
+
+		if ( !this._checkIfRootsExists( Object.keys( initialData ) ) ) {
+			/**
+			 * Cannot init data on a non-existing root. This error is thrown when {@link #init DataController#init() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #init} like:
+			 *
+			 * 		data.init( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-init-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.' );
+		}
 
 		this.model.enqueueChange( 'transparent', writer => {
-			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
+			for ( const rootName of Object.keys( initialData ) ) {
+				const modelRoot = this.model.document.getRoot( rootName );
+				writer.insert( this.parse( initialData[ rootName ], modelRoot ), modelRoot, 0 );
+			}
 		} );
 
 		return Promise.resolve();
@@ -216,19 +262,51 @@ export default class DataController {
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
 	 *
-	 * @param {String} data Input data.
-	 * @param {String} [rootName='main'] Root name.
+	 * When data is passed as a string it is set on a default `main` root:
+	 *
+	 *		dataController.set( '<p>Foo</p>' ); // Sets data on the `main` root.
+	 *
+	 * To set data on a different root or multiple roots at once, object containing `rootName` - `data` pairs should be passed:
+	 *
+	 *		dataController.set( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Sets data on the `main` and `title` roots.
+	 *
+	 * @param {String|Object.<String,String>} data Input data as a string or an object containing `rootName` - `data`
+	 * pairs to set data on multiple roots at once.
 	 */
-	set( data, rootName = 'main' ) {
-		// Save to model.
-		const modelRoot = this.model.document.getRoot( rootName );
+	set( data ) {
+		let newData = {};
+		if ( typeof data === 'string' ) {
+			newData.main = data; // Default root is 'main'. To set data on a different root, object should be passed.
+		} else {
+			newData = data;
+		}
+
+		if ( !this._checkIfRootsExists( Object.keys( newData ) ) ) {
+			/**
+			 * Cannot set data on a non-existing root. This error is thrown when {@link #set DataController#set() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #set} like:
+			 *
+			 * 		data.set( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-set-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.' );
+		}
 
 		this.model.enqueueChange( 'transparent', writer => {
 			writer.setSelection( null );
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
-			writer.remove( writer.createRangeIn( modelRoot ) );
-			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
+			for ( const rootName of Object.keys( newData ) ) {
+				// Save to model.
+				const modelRoot = this.model.document.getRoot( rootName );
+
+				writer.remove( writer.createRangeIn( modelRoot ) );
+				writer.insert( this.parse( newData[ rootName ], modelRoot ), modelRoot, 0 );
+			}
 		} );
 	}
 
@@ -276,6 +354,23 @@ export default class DataController {
 	destroy() {}
 
 	/**
+	 * Checks if all provided root names are existing editor roots.
+	 *
+	 * @private
+	 * @param {Array.<String>} rootNames Root names to check.
+	 * @returns {Boolean} Whether all provided root names are existing editor roots.
+	 */
+	_checkIfRootsExists( rootNames ) {
+		for ( const rootName of rootNames ) {
+			if ( !this.model.document.getRootNames().includes( rootName ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
 	 * Event fired by decorated {@link #init} method.
 	 * See {@link module:utils/observablemixin~ObservableMixin.decorate} for more information and samples.
 	 *

+ 68 - 6
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -178,9 +178,17 @@ describe( 'DataController', () => {
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
 
+		it( 'should set data to multiple roots at once', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+			data.init( { main: 'bar', title: 'baz' } );
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'bar' );
+			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'baz' );
+		} );
+
 		it( 'should get root name as a parameter', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
-			data.init( 'foo', 'title' );
+			data.init( { title: 'foo' } );
 
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'foo' );
 		} );
@@ -210,6 +218,28 @@ describe( 'DataController', () => {
 
 			return promise;
 		} );
+
+		it( 'should throw an error when non-existent root is used (single)', () => {
+			expect( () => {
+				data.init( { nonexistent: '<p>Bar</p>' } );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
+			);
+		} );
+
+		it( 'should throw an error when non-existent root is used (one of many)', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+
+			expect( () => {
+				data.init( { main: 'bar', nonexistent: '<p>Bar</p>' } );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
+			);
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( '' );
+		} );
 	} );
 
 	describe( 'set()', () => {
@@ -240,8 +270,8 @@ describe( 'DataController', () => {
 
 		it( 'should get root name as a parameter', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
-			data.set( 'foo', 'main' );
-			data.set( 'Bar', 'title' );
+			data.set( 'foo' );
+			data.set( { title: 'Bar' } );
 
 			expect( getData( model, { withoutSelection: true, rootName: 'main' } ) ).to.equal( 'foo' );
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
@@ -252,7 +282,7 @@ describe( 'DataController', () => {
 		it( 'should parse given data before set in a context of correct root', () => {
 			schema.extend( '$text', { allowIn: '$title', disallowIn: '$root' } );
 			data.set( 'foo', 'main' );
-			data.set( 'Bar', 'title' );
+			data.set( { title: 'Bar' } );
 
 			expect( getData( model, { withoutSelection: true, rootName: 'main' } ) ).to.equal( '' );
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
@@ -265,14 +295,37 @@ describe( 'DataController', () => {
 		it( 'should allow setting empty data', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 
-			data.set( 'foo', 'title' );
+			data.set( { title: 'foo' } );
 
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'foo' );
 
-			data.set( '', 'title' );
+			data.set( { title: '' } );
 
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( '' );
 		} );
+
+		it( 'should throw an error when non-existent root is used (single)', () => {
+			expect( () => {
+				data.set( { nonexistent: '<p>Bar</p>' } );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
+			);
+		} );
+
+		it( 'should throw an error when non-existent root is used (one of many) without touching any roots data', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+			data.set( 'foo' );
+
+			expect( () => {
+				data.set( { main: 'bar', nonexistent: '<p>Bar</p>' } );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
+			);
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
+		} );
 	} );
 
 	describe( 'get()', () => {
@@ -343,6 +396,15 @@ describe( 'DataController', () => {
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'title' ) ).to.equal( 'Bar' );
 		} );
+
+		it( 'should throw an error when non-existent root is used', () => {
+			expect( () => {
+				data.get( 'nonexistent' );
+			} ).to.throw(
+				CKEditorError,
+				'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.'
+			);
+		} );
 	} );
 
 	describe( 'stringify()', () => {