Oskar Wróbel пре 8 година
родитељ
комит
99b49b9685
2 измењених фајлова са 369 додато и 153 уклоњено
  1. 89 66
      packages/ckeditor5-engine/src/model/batch.js
  2. 280 87
      packages/ckeditor5-engine/tests/model/batch.js

+ 89 - 66
packages/ckeditor5-engine/src/model/batch.js

@@ -30,9 +30,12 @@ import RootAttributeOperation from './operation/rootattributeoperation';
 import DocumentFragment from './documentfragment';
 import DocumentFragment from './documentfragment';
 import Text from './text';
 import Text from './text';
 import Element from './element';
 import Element from './element';
+import RootElement from './rootelement';
 import Position from './position';
 import Position from './position';
 import Range from './range.js';
 import Range from './range.js';
 
 
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 
 /**
 /**
@@ -143,17 +146,17 @@ export default class Batch {
 	insert( item, itemOrPosition, offset ) {
 	insert( item, itemOrPosition, offset ) {
 		const position = Position.createAt( itemOrPosition, offset );
 		const position = Position.createAt( itemOrPosition, offset );
 
 
-		// For text that has no parent we need to make WeakInsert.
+		// For text that has no parent we need to make a WeakInsert.
 		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
 		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
 
 
-		// If item is already in parent.
+		// If item has a parent already.
 		if ( item.parent ) {
 		if ( item.parent ) {
-			// We need to check if item is going to be inserted to the same root.
-			if ( item.root === position.root ) {
+			// We need to check if item is going to be inserted within the same document.
+			if ( isTheSameDocument( item.root, position.root ) ) {
 				// If it's we just need to move it.
 				// If it's we just need to move it.
 				return this.move( Range.createOn( item ), position );
 				return this.move( Range.createOn( item ), position );
 			}
 			}
-			// If it isn't the same root
+			// If it isn't the same root.
 			else {
 			else {
 				// We need to remove this item from old position first.
 				// We need to remove this item from old position first.
 				this.remove( item );
 				this.remove( item );
@@ -224,8 +227,8 @@ export default class Batch {
 	}
 	}
 
 
 	setAttributes( itemOrRange, attributes ) {
 	setAttributes( itemOrRange, attributes ) {
-		for ( const attribute of Object.keys( attributes ) ) {
-			this.setAttribute( itemOrRange, attribute, attributes[ attribute ] );
+		for ( const [ key, val ] of toMap( attributes ) ) {
+			this.setAttribute( itemOrRange, key, val );
 		}
 		}
 	}
 	}
 
 
@@ -265,64 +268,6 @@ export default class Batch {
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * Merges two siblings at the given position.
-	 *
-	 * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
-	 * `batch-merge-no-element-after` error will be thrown.
-	 *
-	 * @chainable
-	 * @param {module:engine/model/position~Position} position Position of merge.
-	 */
-	merge( position ) {
-		const delta = new MergeDelta();
-		this.addDelta( delta );
-
-		const nodeBefore = position.nodeBefore;
-		const nodeAfter = position.nodeAfter;
-
-		if ( !( nodeBefore instanceof Element ) ) {
-			/**
-			 * Node before merge position must be an element.
-			 *
-			 * @error batch-merge-no-element-before
-			 */
-			throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
-		}
-
-		if ( !( nodeAfter instanceof Element ) ) {
-			/**
-			 * Node after merge position must be an element.
-			 *
-			 * @error batch-merge-no-element-after
-			 */
-			throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
-		}
-
-		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
-		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
-
-		const move = new MoveOperation(
-			positionAfter,
-			nodeAfter.maxOffset,
-			positionBefore,
-			this.document.version
-		);
-
-		move.isSticky = true;
-		delta.addOperation( move );
-		this.document.applyOperation( move );
-
-		const graveyard = this.document.graveyard;
-		const gyPosition = new Position( graveyard, [ 0 ] );
-
-		const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
-		delta.addOperation( remove );
-		this.document.applyOperation( remove );
-
-		return this;
-	}
-
 	/**
 	/**
 	 * Moves given {@link module:engine/model/item~Item model item} or given range to target position.
 	 * Moves given {@link module:engine/model/item~Item model item} or given range to target position.
 	 *
 	 *
@@ -340,6 +285,16 @@ export default class Batch {
 
 
 		const position = Position.createAt( itemOrPosition, offset );
 		const position = Position.createAt( itemOrPosition, offset );
 
 
+		if ( !isTheSameDocument( range.root, position.root ) ) {
+			/**
+			 * Range is going to be moved within not the same document. Please use
+			 * {@link module:engine/model/batch~Batch#insert insert} instead.
+			 *
+			 * @error batch-move-different-document
+			 */
+			throw new CKEditorError( 'batch-move-different-document: Range is going to be moved between different documents.' );
+		}
+
 		const delta = new MoveDelta();
 		const delta = new MoveDelta();
 		this.addDelta( delta );
 		this.addDelta( delta );
 
 
@@ -377,12 +332,72 @@ export default class Batch {
 				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
 				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
 			}
 			}
 		} else {
 		} else {
-			addRemoveDelta( Position.createBefore( itemOrRange ), 1 );
+			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
+
+			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
 		}
 		}
 
 
 		return this;
 		return this;
 	}
 	}
 
 
+	/**
+	 * Merges two siblings at the given position.
+	 *
+	 * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
+	 * `batch-merge-no-element-after` error will be thrown.
+	 *
+	 * @chainable
+	 * @param {module:engine/model/position~Position} position Position of merge.
+	 */
+	merge( position ) {
+		const delta = new MergeDelta();
+		this.addDelta( delta );
+
+		const nodeBefore = position.nodeBefore;
+		const nodeAfter = position.nodeAfter;
+
+		if ( !( nodeBefore instanceof Element ) ) {
+			/**
+			 * Node before merge position must be an element.
+			 *
+			 * @error batch-merge-no-element-before
+			 */
+			throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
+		}
+
+		if ( !( nodeAfter instanceof Element ) ) {
+			/**
+			 * Node after merge position must be an element.
+			 *
+			 * @error batch-merge-no-element-after
+			 */
+			throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
+		}
+
+		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
+		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
+
+		const move = new MoveOperation(
+			positionAfter,
+			nodeAfter.maxOffset,
+			positionBefore,
+			this.document.version
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.document.applyOperation( move );
+
+		const graveyard = this.document.graveyard;
+		const gyPosition = new Position( graveyard, [ 0 ] );
+
+		const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
+		delta.addOperation( remove );
+		this.document.applyOperation( remove );
+
+		return this;
+	}
+
 	/**
 	/**
 	 * Renames given element.
 	 * Renames given element.
 	 *
 	 *
@@ -759,3 +774,11 @@ function addMarkerOperation( batch, name, oldRange, newRange ) {
 	delta.addOperation( operation );
 	delta.addOperation( operation );
 	doc.applyOperation( operation );
 	doc.applyOperation( operation );
 }
 }
+
+function isTheSameDocument( rootA, rootB ) {
+	if ( rootA === rootB ) {
+		return true;
+	}
+
+	return rootA instanceof RootElement && rootB instanceof RootElement;
+}

+ 280 - 87
packages/ckeditor5-engine/tests/model/batch.js

@@ -236,57 +236,73 @@ describe( 'Batch', () => {
 			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
 			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
 		} );
 		} );
 
 
-		it( 'should move element from one parent to the other within different root', () => {
-			const parent1 = batch.createDocumentFragment();
-			const parent2 = batch.createDocumentFragment();
-			const b = batch.createText( 'b', { foo: 'bar' } );
+		it( 'should create proper delta for inserting element', () => {
+			const parent = batch.createDocumentFragment();
+			const element = batch.createElement( 'child' );
+
+			const spy = sinon.spy( doc, 'applyOperation' );
+
+			batch.insert( element, parent );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
+			expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
+			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
+		} );
+
+		it( 'should create proper delta for inserting text', () => {
+			const parent = batch.createDocumentFragment();
+			const text = batch.createText( 'child' );
+
+			const spy = sinon.spy( doc, 'applyOperation' );
+
+			batch.insert( text, parent );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
+			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
+		} );
 
 
-			batch
-				.insert( batch.createText( 'a' ), parent1 )
-				.insert( b, parent1, 'end' )
-				.insert( batch.createText( 'c' ), parent1, 'end' );
+		it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
+			const rootA = doc.createRoot();
+			const parent1 = batch.createElement( 'parent' );
+			const parent2 = batch.createElement( 'parent' );
+			const node = batch.createText( 'foo' );
 
 
-			batch.insert( batch.createText( 'ac' ), parent2 );
+			batch.insert( node, parent1 );
+			batch.insert( parent1, rootA );
+			batch.insert( parent2, rootA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.insert( b, parent2, 1 );
+			batch.insert( node, parent2 );
 
 
 			// Verify result.
 			// Verify result.
-			expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] );
-			expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
 
 
 			// Verify deltas and operations.
 			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should move element from one parent to the other within the same root', () => {
-			const root = batch.createDocumentFragment();
-			const parent1 = batch.createElement( 'parent' );
-			const parent2 = batch.createElement( 'parent' );
-			const b = batch.createText( 'b', { foo: 'bar' } );
-
-			batch
-				.insert( parent1, root )
-				.insert( batch.createText( 'a' ), parent1 )
-				.insert( b, parent1, 'end' )
-				.insert( batch.createText( 'c' ), parent1, 'end' );
+		it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
+			const rootA = doc.createRoot( '$root', 'A' );
+			const rootB = doc.createRoot( '$root', 'B' );
+			const node = batch.createText( 'foo' );
 
 
-			batch
-				.insert( parent2, root )
-				.insert( batch.createText( 'ac' ), parent2 );
+			batch.insert( node, rootA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.insert( b, parent2, 1 );
+			batch.insert( node, rootB );
 
 
 			// Verify result.
 			// Verify result.
-			expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] );
-			expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
 
 
 			// Verify deltas and operations.
 			// Verify deltas and operations.
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
@@ -294,51 +310,74 @@ describe( 'Batch', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should create proper delta for element', () => {
-			const parent = batch.createDocumentFragment();
-			const element = batch.createElement( 'child' );
+		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
+			const docFragA = batch.createDocumentFragment();
+			const parent1 = batch.createElement( 'parent' );
+			const parent2 = batch.createElement( 'parent' );
+			const node = batch.createText( 'foo' );
+
+			batch.insert( node, parent1 );
+			batch.insert( parent1, docFragA );
+			batch.insert( parent2, docFragA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.insert( element, parent );
+			batch.insert( node, parent2 );
+
+			// Verify result.
+			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
 
 
+			// Verify deltas and operations.
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should create proper delta for text with no parent', () => {
-			const parent = batch.createDocumentFragment();
-			const text = batch.createText( 'child' );
+		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
+			const root = doc.createRoot();
+			const docFrag = batch.createDocumentFragment();
+			const node = batch.createText( 'foo' );
+
+			batch.insert( node, root );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.insert( text, parent );
+			batch.insert( node, docFrag );
 
 
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
+			// Verify result.
+			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
+
+			// Verify deltas and operations.
+			sinon.assert.calledTwice( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should create proper delta for text with parent', () => {
-			const parent1 = batch.createDocumentFragment();
-			const parent2 = batch.createDocumentFragment();
-			const text = batch.createText( 'child' );
+		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
+			const docFragA = batch.createDocumentFragment();
+			const docFragB = batch.createDocumentFragment();
+			const node = batch.createText( 'foo' );
 
 
-			batch.insert( text, parent1 );
+			batch.insert( node, docFragA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.insert( text, parent2 );
+			batch.insert( node, docFragB );
 
 
+			// Verify result.
+			expect( Array.from( docFragA ) ).to.deep.equal( [] );
+			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
+
+			// Verify deltas and operations.
 			sinon.assert.calledTwice( spy );
 			sinon.assert.calledTwice( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
 		it.skip( 'should transfer markers from given DocumentFragment', () => {
 		it.skip( 'should transfer markers from given DocumentFragment', () => {
@@ -654,53 +693,68 @@ describe( 'Batch', () => {
 			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
 			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should move element from one parent to the other within different root', () => {
-			const parent1 = batch.createDocumentFragment();
-			const parent2 = batch.createDocumentFragment();
-			const b = batch.createText( 'b', { foo: 'bar' } );
+		it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
+			const rootA = doc.createRoot();
+			const parent1 = batch.createElement( 'parent' );
+			const parent2 = batch.createElement( 'parent' );
+			const node = batch.createText( 'foo' );
 
 
-			batch
-				.append( batch.createText( 'a' ), parent1 )
-				.append( b, parent1 )
-				.append( batch.createText( 'c' ), parent1 );
+			batch.insert( node, parent1 );
+			batch.insert( parent1, rootA );
+			batch.insert( parent2, rootA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.append( b, parent2, 1 );
+			batch.append( node, parent2 );
 
 
 			// Verify result.
 			// Verify result.
-			expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] );
-			expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'b' ] );
+			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
 
 
 			// Verify deltas and operations.
 			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
-		it( 'should move element from one parent to the other within the same root', () => {
-			const root = batch.createDocumentFragment();
+		it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
+			const rootA = doc.createRoot( '$root', 'A' );
+			const rootB = doc.createRoot( '$root', 'B' );
+			const node = batch.createText( 'foo' );
+
+			batch.insert( node, rootA );
+
+			const spy = sinon.spy( doc, 'applyOperation' );
+
+			batch.append( node, rootB );
+
+			// Verify result.
+			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
+
+			// Verify deltas and operations.
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
+		} );
+
+		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
+			const docFragA = batch.createDocumentFragment();
 			const parent1 = batch.createElement( 'parent' );
 			const parent1 = batch.createElement( 'parent' );
 			const parent2 = batch.createElement( 'parent' );
 			const parent2 = batch.createElement( 'parent' );
-			const b = batch.createText( 'b', { foo: 'bar' } );
-
-			batch
-				.append( parent1, root )
-				.append( batch.createText( 'a' ), parent1 )
-				.append( b, parent1 )
-				.append( batch.createText( 'c' ), parent1 );
+			const node = batch.createText( 'foo' );
 
 
-			batch.append( parent2, root );
+			batch.insert( node, parent1 );
+			batch.insert( parent1, docFragA );
+			batch.insert( parent2, docFragA );
 
 
 			const spy = sinon.spy( doc, 'applyOperation' );
 			const spy = sinon.spy( doc, 'applyOperation' );
 
 
-			batch.append( b, parent2 );
+			batch.append( node, parent2 );
 
 
 			// Verify result.
 			// Verify result.
-			expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] );
-			expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'b' ] );
+			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
 
 
 			// Verify deltas and operations.
 			// Verify deltas and operations.
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
@@ -708,6 +762,52 @@ describe( 'Batch', () => {
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
 		} );
 		} );
 
 
+		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
+			const root = doc.createRoot();
+			const docFrag = batch.createDocumentFragment();
+			const node = batch.createText( 'foo' );
+
+			batch.insert( node, root );
+
+			const spy = sinon.spy( doc, 'applyOperation' );
+
+			batch.append( node, docFrag );
+
+			// Verify result.
+			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
+			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
+
+			// Verify deltas and operations.
+			sinon.assert.calledTwice( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
+		} );
+
+		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
+			const docFragA = batch.createDocumentFragment();
+			const docFragB = batch.createDocumentFragment();
+			const node = batch.createText( 'foo' );
+
+			batch.insert( node, docFragA );
+
+			const spy = sinon.spy( doc, 'applyOperation' );
+
+			batch.append( node, docFragB );
+
+			// Verify result.
+			expect( Array.from( docFragA ) ).to.deep.equal( [] );
+			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
+
+			// Verify deltas and operations.
+			sinon.assert.calledTwice( spy );
+			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
+			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
+			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
+			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
+		} );
+
 		it( 'should be chainable', () => {
 		it( 'should be chainable', () => {
 			expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch );
 			expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch );
 		} );
 		} );
@@ -1209,6 +1309,91 @@ describe( 'Batch', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'setAttributes()', () => {
+		let doc, batch, frag, item;
+
+		beforeEach( () => {
+			doc = new Document();
+			batch = doc.batch();
+
+			frag = batch.createDocumentFragment();
+			item = batch.createText( 'xxx', { b: 2, c: 3 } );
+
+			batch.appendText( 'xxx', { a: 1 }, frag );
+			batch.append( item, frag );
+		} );
+
+		it( 'should set attributes one by one on range', () => {
+			const range = Range.createIn( frag );
+
+			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
+			// such a big amount of the same tests, so let's use a spy here.
+			const spy = sinon.spy( batch, 'setAttribute' );
+
+			batch.setAttributes( range, { a: 3, c: null } );
+
+			// Verify result.
+			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
+			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
+
+			// Verify operations
+			sinon.assert.calledTwice( spy );
+			sinon.assert.calledWith( spy.firstCall, range, 'a', 3 );
+			sinon.assert.calledWith( spy.secondCall, range, 'c', null );
+		} );
+
+		it( 'should set attributes one by one on range for map as attributes list', () => {
+			const range = Range.createIn( frag );
+
+			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
+			// such a big amount of the same tests, so let's use a spy here.
+			const spy = sinon.spy( batch, 'setAttribute' );
+
+			batch.setAttributes( range, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) );
+
+			// Verify result.
+			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
+			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
+
+			// Verify operations
+			sinon.assert.calledTwice( spy );
+			sinon.assert.calledWith( spy.firstCall, range, 'a', 3 );
+			sinon.assert.calledWith( spy.secondCall, range, 'c', null );
+		} );
+
+		it( 'should set attributes one by one on item', () => {
+			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
+			// such a big amount of the same tests, so let's use a spy here.
+			const spy = sinon.spy( batch, 'setAttribute' );
+
+			batch.setAttributes( item, { a: 3, c: null } );
+
+			// Verify result.
+			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
+
+			// Verify operations
+			sinon.assert.calledTwice( spy );
+			sinon.assert.calledWith( spy.firstCall, item, 'a', 3 );
+			sinon.assert.calledWith( spy.secondCall, item, 'c', null );
+		} );
+
+		it( 'should set attributes one by one on item for maps as attributes list', () => {
+			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
+			// such a big amount of the same tests, so let's use a spy here.
+			const spy = sinon.spy( batch, 'setAttribute' );
+
+			batch.setAttributes( item, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) );
+
+			// Verify result.
+			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
+
+			// Verify operations
+			sinon.assert.calledTwice( spy );
+			sinon.assert.calledWith( spy.firstCall, item, 'a', 3 );
+			sinon.assert.calledWith( spy.secondCall, item, 'c', null );
+		} );
+	} );
+
 	describe( 'merge()', () => {
 	describe( 'merge()', () => {
 		let doc, root, p1, p2, batch;
 		let doc, root, p1, p2, batch;
 
 
@@ -1299,6 +1484,14 @@ describe( 'Batch', () => {
 			} ).to.throw( CKEditorError, /^batch-move-range-not-flat/ );
 			} ).to.throw( CKEditorError, /^batch-move-range-not-flat/ );
 		} );
 		} );
 
 
+		it( 'should throw if range is going to be moved to the other document', () => {
+			const docFrag = batch.createDocumentFragment();
+
+			expect( () => {
+				doc.batch().move( range, docFrag );
+			} ).to.throw( CKEditorError, /^batch-move-different-document/ );
+		} );
+
 		it( 'should be chainable', () => {
 		it( 'should be chainable', () => {
 			chain = batch.move( range, new Position( root, [ 1, 3 ] ) );
 			chain = batch.move( range, new Position( root, [ 1, 3 ] ) );