Selaa lähdekoodia

Merge branch 'master' into t/1177

Kamil Piechaczek 8 vuotta sitten
vanhempi
commit
d6289556f0
75 muutettua tiedostoa jossa 3634 lisäystä ja 2144 poistoa
  1. 10 8
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 2 2
      packages/ckeditor5-engine/src/controller/deletecontent.js
  3. 4 4
      packages/ckeditor5-engine/src/controller/insertcontent.js
  4. 13 10
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  5. 2 8
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  6. 33 30
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  7. 11 1
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  8. 9 7
      packages/ckeditor5-engine/src/dev-utils/model.js
  9. 862 54
      packages/ckeditor5-engine/src/model/batch.js
  10. 0 149
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  11. 0 1
      packages/ckeditor5-engine/src/model/delta/basic-deltas.js
  12. 0 50
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  13. 0 85
      packages/ckeditor5-engine/src/model/delta/markerdelta.js
  14. 0 65
      packages/ckeditor5-engine/src/model/delta/mergedelta.js
  15. 0 41
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  16. 0 38
      packages/ckeditor5-engine/src/model/delta/removedelta.js
  17. 0 37
      packages/ckeditor5-engine/src/model/delta/renamedelta.js
  18. 30 0
      packages/ckeditor5-engine/src/model/delta/rootattributedelta.js
  19. 0 55
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  20. 0 51
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  21. 0 40
      packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js
  22. 0 65
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  23. 9 7
      packages/ckeditor5-engine/src/model/document.js
  24. 5 5
      packages/ckeditor5-engine/src/model/documentselection.js
  25. 5 0
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  26. 87 0
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  27. 5 0
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  28. 23 0
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  29. 11 0
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  30. 12 0
      packages/ckeditor5-engine/src/model/operation/nooperation.js
  31. 10 0
      packages/ckeditor5-engine/src/model/operation/operation.js
  32. 31 0
      packages/ckeditor5-engine/src/model/operation/reinsertoperation.js
  33. 35 0
      packages/ckeditor5-engine/src/model/operation/removeoperation.js
  34. 5 0
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  35. 8 0
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  36. 3 10
      packages/ckeditor5-engine/src/model/schema.js
  37. 22 14
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  38. 8 5
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  39. 4 2
      packages/ckeditor5-engine/tests/controller/insertcontent.js
  40. 20 19
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js
  41. 41 33
      packages/ckeditor5-engine/tests/conversion/buildviewconverter.js
  42. 26 18
      packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js
  43. 13 9
      packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js
  44. 24 15
      packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js
  45. 36 1
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  46. 12 12
      packages/ckeditor5-engine/tests/dev-utils/model.js
  47. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  48. 1868 42
      packages/ckeditor5-engine/tests/model/batch.js
  49. 1 405
      packages/ckeditor5-engine/tests/model/delta/attributedelta.js
  50. 0 88
      packages/ckeditor5-engine/tests/model/delta/insertdelta.js
  51. 0 111
      packages/ckeditor5-engine/tests/model/delta/markerdelta.js
  52. 0 62
      packages/ckeditor5-engine/tests/model/delta/mergedelta.js
  53. 0 67
      packages/ckeditor5-engine/tests/model/delta/movedelta.js
  54. 0 71
      packages/ckeditor5-engine/tests/model/delta/removedelta.js
  55. 0 46
      packages/ckeditor5-engine/tests/model/delta/renamedelta.js
  56. 12 0
      packages/ckeditor5-engine/tests/model/delta/rootattributedelta.js
  57. 0 78
      packages/ckeditor5-engine/tests/model/delta/splitdelta.js
  58. 0 50
      packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js
  59. 0 50
      packages/ckeditor5-engine/tests/model/delta/weakinsertdelta.js
  60. 0 83
      packages/ckeditor5-engine/tests/model/delta/wrapdelta.js
  61. 34 2
      packages/ckeditor5-engine/tests/model/document/document.js
  62. 5 5
      packages/ckeditor5-engine/tests/model/documentselection.js
  63. 1 1
      packages/ckeditor5-engine/tests/model/markercollection.js
  64. 29 0
      packages/ckeditor5-engine/tests/model/operation/attributeoperation.js
  65. 55 0
      packages/ckeditor5-engine/tests/model/operation/detachoperation.js
  66. 24 0
      packages/ckeditor5-engine/tests/model/operation/insertoperation.js
  67. 20 0
      packages/ckeditor5-engine/tests/model/operation/markeroperation.js
  68. 28 0
      packages/ckeditor5-engine/tests/model/operation/moveoperation.js
  69. 4 0
      packages/ckeditor5-engine/tests/model/operation/nooperation.js
  70. 35 0
      packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js
  71. 31 0
      packages/ckeditor5-engine/tests/model/operation/removeoperation.js
  72. 19 0
      packages/ckeditor5-engine/tests/model/operation/renameoperation.js
  73. 28 0
      packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js
  74. 4 27
      packages/ckeditor5-engine/tests/model/schema/schema.js
  75. 4 4
      packages/ckeditor5-engine/tests/model/selection.js

+ 10 - 8
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -21,7 +21,6 @@ import { convertText, convertToModelFragment } from '../conversion/view-to-model
 import ViewDocumentFragment from '../view/documentfragment';
 
 import ModelRange from '../model/range';
-import ModelPosition from '../model/position';
 import ModelElement from '../model/element';
 
 import insertContent from './insertcontent';
@@ -196,9 +195,10 @@ export default class DataController {
 			this.model.selection.clearAttributes();
 
 			// Initial batch should be ignored by features like undo, etc.
-			this.model.batch( 'transparent' )
-				.remove( ModelRange.createIn( modelRoot ) )
-				.insert( ModelPosition.createAt( modelRoot, 0 ), this.parse( data ) );
+			const batch = this.model.batch( 'transparent' );
+
+			batch.remove( ModelRange.createIn( modelRoot ) );
+			batch.insert( this.parse( data, batch ), modelRoot );
 		} );
 	}
 
@@ -208,16 +208,17 @@ export default class DataController {
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
 	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
 	 */
-	parse( data, context = '$root' ) {
+	parse( data, batch, context = '$root' ) {
 		// data -> view
 		const viewDocumentFragment = this.processor.toView( data );
 
 		// view -> model
-		return this.toModel( viewDocumentFragment, context );
+		return this.toModel( viewDocumentFragment, batch, context );
 	}
 
 	/**
@@ -231,12 +232,13 @@ export default class DataController {
 	 *
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
 	 * Element or document fragment which content will be converted.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
 	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
-	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
+	toModel( viewElementOrFragment, batch, context = '$root' ) {
+		return this.viewToModel.convert( viewElementOrFragment, batch, { context: [ context ] } );
 	}
 
 	/**

+ 2 - 2
packages/ckeditor5-engine/src/controller/deletecontent.js

@@ -128,7 +128,7 @@ function mergeBranches( batch, startPos, endPos ) {
 		// <a><b>x[]</b></a><c><d>{}y</d></c>
 		// becomes:
 		// <a><b>x</b>[]<d>y</d></a><c>{}</c>
-		batch.move( endParent, startPos );
+		batch.insert( endParent, startPos );
 	}
 
 	// Merge two siblings:
@@ -181,7 +181,7 @@ function checkCanBeMerged( leftPos, rightPos ) {
 
 function insertParagraph( batch, position, selection ) {
 	const paragraph = new Element( 'paragraph' );
-	batch.insert( position, paragraph );
+	batch.insert( paragraph, position );
 
 	selection.setCollapsedAt( paragraph );
 }

+ 4 - 4
packages/ckeditor5-engine/src/controller/insertcontent.js

@@ -229,7 +229,7 @@ class Insertion {
 		// If the node is a text and bare text is allowed in current position it means that the node
 		// contains disallowed attributes and we have to remove them.
 		else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
-			this.schema.removeDisallowedAttributes( [ node ], this.position );
+			this.schema.removeDisallowedAttributes( [ node ], this.position, this.batch );
 			this._handleNode( node, context );
 		}
 		// If text is not allowed, try autoparagraphing.
@@ -256,7 +256,7 @@ class Insertion {
 
 		const livePos = LivePosition.createFromPosition( this.position );
 
-		this.batch.insert( this.position, node );
+		this.batch.insert( node, this.position );
 
 		this.position = Position.createFromPosition( livePos );
 		livePos.detach();
@@ -341,7 +341,7 @@ class Insertion {
 	 * @param {Object} context
 	 */
 	_tryAutoparagraphing( node, context ) {
-		const paragraph = new Element( 'paragraph' );
+		const paragraph = this.batch.createElement( 'paragraph' );
 
 		// Do not autoparagraph if the paragraph won't be allowed there,
 		// cause that would lead to an infinite loop. The paragraph would be rejected in
@@ -350,7 +350,7 @@ class Insertion {
 			// When node is a text and is disallowed by schema it means that contains disallowed attributes
 			// and we need to remove them.
 			if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
-				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] );
+				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ], this.batch );
 			}
 
 			if ( this._checkIsAllowed( node, [ paragraph ] ) ) {

+ 13 - 10
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -8,9 +8,6 @@
  */
 
 import Matcher from '../view/matcher';
-import ModelElement from '../model/element';
-import ModelPosition from '../model/position';
-import modelWriter from '../model/writer';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
@@ -266,13 +263,15 @@ class ViewConverterBuilder {
 	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 	 *		buildViewConverter().for( dispatcher )
 	 *			.fromElement( 'img' )
-	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } );
+	 *			.toElement( ( viewElement, batch ) => batch.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
 	 *
 	 * @param {String|Function} element Model element name or model element creator function.
 	 */
 	toElement( element ) {
 		function eventCallbackGen( from ) {
 			return ( evt, data, consumable, conversionApi ) => {
+				const batch = conversionApi.batch;
+
 				// There is one callback for all patterns in the matcher.
 				// This will be usually just one pattern but we support matchers with many patterns too.
 				const matchAll = from.matcher.matchAll( data.input );
@@ -285,7 +284,7 @@ class ViewConverterBuilder {
 				// Now, for every match between matcher and actual element, we will try to consume the match.
 				for ( const match of matchAll ) {
 					// Create model element basing on creator function or element name.
-					const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element );
+					const modelElement = element instanceof Function ? element( data.input, batch ) : batch.createElement( element );
 
 					// Do not convert if element building function returned falsy value.
 					if ( !modelElement ) {
@@ -310,8 +309,10 @@ class ViewConverterBuilder {
 
 					// Convert children of converted view element and append them to `modelElement`.
 					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
-					const insertPosition = ModelPosition.createAt( modelElement, 'end' );
-					modelWriter.insert( insertPosition, modelChildren );
+
+					for ( const child of Array.from( modelChildren ) ) {
+						batch.append( child, modelElement );
+					}
 
 					// Remove created `modelElement` from the parents stack.
 					data.context.pop();
@@ -433,7 +434,9 @@ class ViewConverterBuilder {
 	 */
 	toMarker( creator ) {
 		function eventCallbackGen( from ) {
-			return ( evt, data, consumable ) => {
+			return ( evt, data, consumable, conversionApi ) => {
+				const batch = conversionApi.batch;
+
 				// There is one callback for all patterns in the matcher.
 				// This will be usually just one pattern but we support matchers with many patterns too.
 				const matchAll = from.matcher.matchAll( data.input );
@@ -450,7 +453,7 @@ class ViewConverterBuilder {
 					modelElement = creator( data.input );
 				// When there is no creator then create model element basing on data from view element.
 				} else {
-					modelElement = new ModelElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
+					modelElement = batch.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
 				}
 
 				// Check if model element is correct (has proper name and property).
@@ -525,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) {
 	};
 
 	if ( conversionApi.schema.check( schemaQuery ) ) {
-		toChange.setAttribute( attribute.key, attribute.value );
+		conversionApi.batch.setAttribute( attribute.key, attribute.value, toChange );
 	}
 }
 

+ 2 - 8
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -3,10 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocumentFragment from '../model/documentfragment';
-import ModelText from '../model/text';
-import { normalizeNodes } from '../model/writer';
-
 /**
  * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
  * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
@@ -33,9 +29,7 @@ export function convertToModelFragment() {
 	return ( evt, data, consumable, conversionApi ) => {
 		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
 		if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
-			const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
-
-			data.output = new ModelDocumentFragment( normalizeNodes( convertedChildren ) );
+			data.output = conversionApi.convertChildren( data.input, consumable, data );
 		}
 	};
 }
@@ -54,7 +48,7 @@ export function convertText() {
 
 		if ( conversionApi.schema.check( schemaQuery ) ) {
 			if ( consumable.consume( data.input ) ) {
-				data.output = new ModelText( data.input.data );
+				data.output = conversionApi.batch.createText( data.input.data );
 			}
 		}
 	};

+ 33 - 30
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -13,11 +13,9 @@ import ModelPosition from '../model/position';
 import ModelTreeWalker from '../model/treewalker';
 import ModelNode from '../model/node';
 import ModelDocumentFragment from '../model/documentfragment';
-import { remove } from '../model/writer';
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 
 /**
@@ -93,7 +91,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  *		// Fire conversion.
  *		// Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
  *		// is going to be appended directly to a '$root' element, use that in `context`.
- *		viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
+ *		viewDispatcher.convert( viewDocumentFragment, batch, { context: [ '$root' ] } );
  *
  * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
  * event which can be used to prepare tree view for conversion.
@@ -118,12 +116,15 @@ export default class ViewConversionDispatcher {
 		 *
 		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
 		 */
-		this.conversionApi = extend( {}, conversionApi );
+		this.conversionApi = Object.assign( {}, conversionApi );
 
 		// `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and
 		// set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
+
+		// Batch used for conversion. Is passed to #convert method and removed at the and of the conversion.
+		this.conversionApi.batch = null;
 	}
 
 	/**
@@ -134,13 +135,17 @@ export default class ViewConversionDispatcher {
 	 * @fires documentFragment
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
 	 * Part of the view to be converted.
-	 * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
+	 * @param {Object} additionalData Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
 	 * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
 	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
 	 */
-	convert( viewItem, additionalData = {} ) {
+	convert( viewItem, batch, additionalData ) {
+		// Store batch in current conversion as conversionApi, will be removed at the end of this conversion.
+		this.conversionApi.batch = batch;
+
 		this.fire( 'viewCleanup', viewItem );
 
 		const consumable = ViewConsumable.createFrom( viewItem );
@@ -149,16 +154,19 @@ export default class ViewConversionDispatcher {
 		// We can get a null here if conversion failed (see _convertItem())
 		// or simply if an item could not be converted (e.g. due to the schema).
 		if ( !conversionResult ) {
-			return new ModelDocumentFragment();
+			return batch.createDocumentFragment();
 		}
 
 		// When conversion result is not a document fragment we need to wrap it in document fragment.
 		if ( !conversionResult.is( 'documentFragment' ) ) {
-			conversionResult = new ModelDocumentFragment( [ conversionResult ] );
+			const docFrag = batch.createDocumentFragment();
+
+			batch.append( conversionResult, docFrag );
+			conversionResult = docFrag;
 		}
 
 		// Extract temporary markers elements from model and set as static markers collection.
-		conversionResult.markers = extractMarkersFromModelFragment( conversionResult );
+		conversionResult.markers = extractMarkersFromModelFragment( conversionResult, batch );
 
 		return conversionResult;
 	}
@@ -168,7 +176,7 @@ export default class ViewConversionDispatcher {
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
 	 */
 	_convertItem( input, consumable, additionalData = {} ) {
-		const data = extend( {}, additionalData, {
+		const data = Object.assign( {}, additionalData, {
 			input,
 			output: null
 		} );
@@ -203,24 +211,19 @@ export default class ViewConversionDispatcher {
 	 * @private
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
 	 */
-	_convertChildren( input, consumable, additionalData = {} ) {
-		// Get all children of view input item.
-		const viewChildren = Array.from( input.getChildren() );
-
-		// 1. Map those children to model.
-		// 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged).
-		// 3. Extract children from document fragments to flatten results.
-		const convertedChildren = viewChildren
-			.map( viewChild => this._convertItem( viewChild, consumable, additionalData ) )
-			.filter( converted => converted instanceof ModelNode || converted instanceof ModelDocumentFragment )
-			.reduce( ( result, filtered ) => {
-				return result.concat(
-					filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered
-				);
-			}, [] );
-
-		// Normalize array to model document fragment.
-		return new ModelDocumentFragment( convertedChildren );
+	_convertChildren( input, consumable, additionalData ) {
+		const batch = this.conversionApi.batch;
+		const documentFragment = batch.createDocumentFragment();
+
+		for ( const viewChild of Array.from( input.getChildren() ) ) {
+			const modelChild = this._convertItem( viewChild, consumable, additionalData );
+
+			if ( modelChild instanceof ModelNode || modelChild instanceof ModelDocumentFragment ) {
+				batch.append( modelChild, documentFragment );
+			}
+		}
+
+		return documentFragment;
 	}
 
 	/**
@@ -278,7 +281,7 @@ mix( ViewConversionDispatcher, EmitterMixin );
 //
 // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
 // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
-function extractMarkersFromModelFragment( modelItem ) {
+function extractMarkersFromModelFragment( modelItem, batch ) {
 	const markerElements = new Set();
 	const markers = new Map();
 
@@ -310,7 +313,7 @@ function extractMarkersFromModelFragment( modelItem ) {
 		}
 
 		// Remove marker element from DocumentFragment.
-		remove( ModelRange.createOn( markerElement ) );
+		batch.remove( markerElement );
 	}
 
 	return markers;

+ 11 - 1
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -18,6 +18,7 @@ import ModelTextProxy from '../model/textproxy';
 import ModelElement from '../model/element';
 import Operation from '../model/operation/operation';
 import AttributeOperation from '../model/operation/attributeoperation';
+import DetachOperation from '../model/operation/detachoperation';
 import InsertOperation from '../model/operation/insertoperation';
 import MarkerOperation from '../model/operation/markeroperation';
 import MoveOperation from '../model/operation/moveoperation';
@@ -25,12 +26,13 @@ import NoOperation from '../model/operation/nooperation';
 import RenameOperation from '../model/operation/renameoperation';
 import RootAttributeOperation from '../model/operation/rootattributeoperation';
 import Delta from '../model/delta/delta';
-import { default as AttributeDelta, RootAttributeDelta } from '../model/delta/attributedelta';
+import AttributeDelta from '../model/delta/attributedelta';
 import InsertDelta from '../model/delta/insertdelta';
 import MarkerDelta from '../model/delta/markerdelta';
 import MergeDelta from '../model/delta/mergedelta';
 import MoveDelta from '../model/delta/movedelta';
 import RenameDelta from '../model/delta/renamedelta';
+import RootAttributeDelta from '../model/delta/rootattributedelta';
 import SplitDelta from '../model/delta/splitdelta';
 import UnwrapDelta from '../model/delta/unwrapdelta';
 import WrapDelta from '../model/delta/wrapdelta';
@@ -273,6 +275,14 @@ function enableLoggingTools() {
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
 	};
 
+	DetachOperation.prototype.toString = function() {
+		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
+		const nodes = Array.from( range.getItems() );
+		const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
+
+		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
+	};
+
 	InsertOperation.prototype.toString = function() {
 		const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
 

+ 9 - 7
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -96,8 +96,10 @@ export function setData( document, data, options = {} ) {
 	let modelDocumentFragment, selection;
 	const modelRoot = document.getRoot( options.rootName || 'main' );
 
+	const batch = document.batch( options.batchType || 'transparent' );
+
 	// Parse data string to model.
-	const parsedResult = setData._parse( data, document.schema, {
+	const parsedResult = setData._parse( data, document.schema, batch, {
 		lastRangeBackward: options.lastRangeBackward,
 		selectionAttributes: options.selectionAttributes,
 		context: [ modelRoot.name ]
@@ -113,9 +115,8 @@ export function setData( document, data, options = {} ) {
 
 	document.enqueueChanges( () => {
 		// Replace existing model in document by new one.
-		document.batch( options.batchType || 'transparent' )
-			.remove( ModelRange.createIn( modelRoot ) )
-			.insert( ModelPosition.createAt( modelRoot, 0 ), modelDocumentFragment );
+		batch.remove( ModelRange.createIn( modelRoot ) );
+		batch.insert( modelDocumentFragment, modelRoot );
 
 		// Clean up previous document selection.
 		document.selection.clearAttributes();
@@ -242,7 +243,8 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  *
  * @param {String} data HTML-like string to be parsed.
  * @param {module:engine/model/schema~Schema} schema Schema instance uses by converters for element validation.
- * @param {Object} options Additional configuration.
+ * @param {module:engine/model/batch~Batch} batch Batch used for conversion.
+ * @param {Object} [options={}] Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
  * @param {module:engine/model/schema~SchemaPath} [options.context=[ '$root' ]] The conversion context.
@@ -251,7 +253,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  * module:engine/model/documentfragment~DocumentFragment|Object} Returns parsed model node or
  * object with two fields `model` and `selection` when selection ranges were included in data to parse.
  */
-export function parse( data, schema, options = {} ) {
+export function parse( data, schema, batch, options = {} ) {
 	const mapper = new Mapper();
 
 	// Replace not accepted by XML `$text` tag name by valid one `model-text-with-attributes`.
@@ -282,7 +284,7 @@ export function parse( data, schema, options = {} ) {
 	viewToModel.on( 'text', convertToModelText() );
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
+	let model = viewToModel.convert( viewDocumentFragment.root, batch, { context: options.context || [ '$root' ] } );
 
 	// If root DocumentFragment contains only one element - return that element.
 	if ( model.is( 'documentFragment' ) && model.childCount == 1 ) {

+ 862 - 54
packages/ckeditor5-engine/src/model/batch.js

@@ -7,6 +7,37 @@
  * @module engine/model/batch
  */
 
+import AttributeDelta from './delta/attributedelta';
+import InsertDelta from './delta/insertdelta';
+import MarkerDelta from './delta/markerdelta';
+import MergeDelta from './delta/mergedelta';
+import MoveDelta from './delta/movedelta';
+import RemoveDelta from './delta/removedelta';
+import RenameDelta from './delta/renamedelta';
+import RootAttributeDelta from './delta/rootattributedelta';
+import SplitDelta from './delta/splitdelta';
+import UnwrapDelta from './delta/unwrapdelta';
+import WeakInsertDelta from './delta/weakinsertdelta';
+import WrapDelta from './delta/wrapdelta';
+
+import AttributeOperation from './operation/attributeoperation';
+import DetachOperation from './operation/detachoperation';
+import InsertOperation from './operation/insertoperation';
+import MarkerOperation from './operation/markeroperation';
+import MoveOperation from './operation/moveoperation';
+import RemoveOperation from './operation/removeoperation';
+import RenameOperation from './operation/renameoperation';
+import RootAttributeOperation from './operation/rootattributeoperation';
+
+import DocumentFragment from './documentfragment';
+import Text from './text';
+import Element from './element';
+import RootElement from './rootelement';
+import Position from './position';
+import Range from './range.js';
+
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
@@ -16,18 +47,15 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  *
  * For example to create two separate undo steps you can call:
  *
- *		doc.batch().insert( firstPosition, 'foo' );
- *		doc.batch().insert( secondPosition, 'bar' );
+ *		doc.batch().insert( 'foo', firstPosition );
+ *		doc.batch().insert( 'bar', secondPosition );
  *
  * To create a single undo step:
  *
  *		const batch = doc.batch();
- *		batch.insert( firstPosition, 'foo' );
- *		batch.insert( secondPosition, 'bar' );
- *
- * Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to:
+ *		batch.insert( 'foo', firstPosition );
+ *		batch.insert( 'bar', secondPosition );
  *
- *		doc.batch().insert( firstPosition, 'foo' ).insert( secondPosition, 'bar' );
  */
 export default class Batch {
 	/**
@@ -101,55 +129,835 @@ export default class Batch {
 			yield* delta.operations;
 		}
 	}
+
+	/**
+	 * Creates a new {@link module:engine/model/text~Text text node}.
+	 *
+	 *		batch.createText( 'foo' );
+	 *		batch.createText( 'foo', { 'bold': true } );
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @returns {module:engine/model/text~Text} Created text node.
+	 */
+	createText( data, attributes ) {
+		return new Text( data, attributes );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/model/element~Element element}.
+	 *
+	 *		batch.createElement( 'paragraph' );
+	 *		batch.createElement( 'paragraph', { 'alignment': 'center' } );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @returns {module:engine/model/element~Element} Created element.
+	 */
+	createElement( name, attributes ) {
+		return new Element( name, attributes );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
+	 *
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
+	 */
+	createDocumentFragment() {
+		return new DocumentFragment();
+	}
+
+	/**
+	 * Inserts item on given position.
+	 *
+	 *		const paragraph = batch.createElement( 'paragraph' );
+	 *		batch.insert( paragraph, position );
+	 *
+	 * Instead of using position you can use parent and offset:
+	 *
+	 * 		const text = batch.createText( 'foo' );
+	 *		batch.insert( text, paragraph, 5 );
+	 *
+	 * You can also use `end` instead of the offset to insert at the end:
+	 *
+	 * 		const text = batch.createText( 'foo' );
+	 *		batch.insert( text, paragraph, 'end' );
+	 *
+	 * Or insert before or after another element:
+	 *
+	 * 		const paragraph = batch.createElement( 'paragraph' );
+	 *		batch.insert( paragraph, anotherParagraph, 'after' );
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
+	 * item Item or document fragment to insert.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insert( item, itemOrPosition, offset ) {
+		const position = Position.createAt( itemOrPosition, offset );
+
+		// For text that has no parent we need to make a WeakInsert.
+		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
+
+		// If item has a parent already.
+		if ( item.parent ) {
+			// We need to check if item is going to be inserted within the same document.
+			if ( isSameTree( item.root, position.root ) ) {
+				// If it's we just need to move it.
+				this.move( Range.createOn( item ), position );
+
+				return;
+			}
+			// If it isn't the same root.
+			else {
+				// We need to remove this item from old position first.
+				this.remove( item );
+			}
+		}
+
+		const insert = new InsertOperation( position, item, this.document.version );
+
+		this.addDelta( delta );
+		delta.addOperation( insert );
+		this.document.applyOperation( insert );
+
+		// When element is a DocumentFragment we need to move its markers to Document#markers.
+		if ( item instanceof DocumentFragment ) {
+			for ( const [ markerName, markerRange ] of item.markers ) {
+				// We need to migrate marker range from DocumentFragment to Document.
+				const rangeRootPosition = Position.createAt( markerRange.root );
+				const range = new Range(
+					markerRange.start._getCombined( rangeRootPosition, position ),
+					markerRange.end._getCombined( rangeRootPosition, position )
+				);
+
+				this.setMarker( markerName, range );
+			}
+		}
+	}
+
+	/**
+	 * Creates and inserts text on given position. You can optionally set text attributes:
+	 *
+	 *		batch.insertText( 'foo', position );
+	 *		batch.insertText( 'foo', { 'bold': true }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		batch.insertText( 'foo', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		batch.insertText( 'foo', paragraph, 'end' ); // inserts at the end of the paragraph
+	 *		batch.insertText( 'foo', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertText( text, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createText( text ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createText( text, attributes ), itemOrPosition, offset );
+		}
+	}
+
+	/**
+	 * Creates and inserts element on given position. You can optionally set attributes:
+	 *
+	 *		batch.insertElement( 'paragraph', position );
+	 *		batch.insertElement( 'paragraph', { 'alignment': 'center' }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		batch.insertElement( 'paragraph', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		batch.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
+	 *		batch.insertElement( 'paragraph', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertElement( name, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createElement( name ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
+		}
+	}
+
+	/**
+	 * Inserts item at the end of the given parent.
+	 *
+	 *		const paragraph = batch.createElement( 'paragraph' );
+	 *		batch.append( paragraph, root );
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
+	 * item Item or document fragment to insert.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	append( item, parent ) {
+		this.insert( item, parent, 'end' );
+	}
+
+	/**
+	 * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
+	 *
+	 *		batch.appendText( 'foo', paragraph );
+	 *		batch.appendText( 'foo', { 'bold': true }, paragraph );
+	 *
+	 * @param {String} text Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendText( text, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createText( text ), attributes, 'end' );
+		} else {
+			this.insert( this.createText( text, attributes ), parent, 'end' );
+		}
+	}
+
+	/**
+	 * Creates element and inserts it at the end of the parent. You can optionally set attributes:
+	 *
+	 *		batch.appendElement( 'paragraph', root );
+	 *		batch.appendElement( 'paragraph', { 'alignment': 'center' }, root );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendElement( name, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createElement( name ), attributes, 'end' );
+		} else {
+			this.insert( this.createElement( name, attributes ), parent, 'end' );
+		}
+	}
+
+	/**
+	 * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {*} value Attribute new value.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attribute will be set.
+	 */
+	setAttribute( key, value, itemOrRange ) {
+		if ( itemOrRange instanceof Range ) {
+			setAttributeToRange( this, key, value, itemOrRange );
+		} else {
+			setAttributeToItem( this, key, value, itemOrRange );
+		}
+	}
+
+	/**
+	 * Sets values of attributes on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 *		batch.setAttributes( {
+	 *			'bold': true,
+	 *			'italic': true
+	 *		}, range );
+	 *
+	 * @param {Object} attributes Attributes keys and values.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attributes will be set.
+	 */
+	setAttributes( attributes, itemOrRange ) {
+		for ( const [ key, val ] of toMap( attributes ) ) {
+			this.setAttribute( key, val, itemOrRange );
+		}
+	}
+
+	/**
+	 * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
+	 * or from a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which the attribute will be removed.
+	 */
+	removeAttribute( key, itemOrRange ) {
+		if ( itemOrRange instanceof Range ) {
+			setAttributeToRange( this, key, null, itemOrRange );
+		} else {
+			setAttributeToItem( this, key, null, itemOrRange );
+		}
+	}
+
+	/**
+	 * Removes all attributes from all elements in the range or from the given item.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which all attributes will be removed.
+	 */
+	clearAttributes( itemOrRange ) {
+		const removeAttributesFromItem = item => {
+			for ( const attribute of item.getAttributeKeys() ) {
+				this.removeAttribute( attribute, item );
+			}
+		};
+
+		if ( !( itemOrRange instanceof Range ) ) {
+			removeAttributesFromItem( itemOrRange );
+		} else {
+			for ( const item of itemOrRange.getItems() ) {
+				removeAttributesFromItem( item );
+			}
+		}
+	}
+
+	/**
+	 * Moves all items in the source range to the target position.
+	 *
+	 *		batch.move( sourceRange, targetPosition );
+	 *
+	 * Instead of the target position you can use parent and offset or define that range should be moved to the end
+	 * or before or after chosen item:
+	 *
+	 * 		batch.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
+	 *		batch.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
+	 *		batch.move( sourceRange, image, 'after' ); // moves all items in the range after the image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that items can be moved only within the same tree. It means that you can move items within the same root
+	 * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
+	 * but you can not move items from document fragment to the document or from one detached element to another. Use
+	 * {@link module:engine/model/batch~Batch#insert} in such cases.
+	 *
+	 * @param {module:engine/model/range~Range} range Source range.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	move( range, itemOrPosition, offset ) {
+		if ( !( range instanceof Range ) ) {
+			/**
+			 * Invalid range to move.
+			 *
+			 * @error batch-move-invalid-range
+			 */
+			throw new CKEditorError( 'batch-move-invalid-range: Invalid range to move.' );
+		}
+
+		if ( !range.isFlat ) {
+			/**
+			 * Range to move is not flat.
+			 *
+			 * @error batch-move-range-not-flat
+			 */
+			throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
+		}
+
+		const position = Position.createAt( itemOrPosition, offset );
+
+		if ( !isSameTree( 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();
+		this.addDelta( delta );
+
+		const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version );
+		delta.addOperation( operation );
+		this.document.applyOperation( operation );
+	}
+
+	/**
+	 * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
+	 */
+	remove( itemOrRange ) {
+		const addRemoveDelta = ( position, howMany ) => {
+			const delta = new RemoveDelta();
+			this.addDelta( delta );
+			let operation;
+
+			if ( position.root.document ) {
+				const graveyard = this.document.graveyard;
+				const gyPosition = new Position( graveyard, [ 0 ] );
+
+				operation = new RemoveOperation( position, howMany, gyPosition, this.document.version );
+			} else {
+				operation = new DetachOperation( position, howMany, this.document.version );
+			}
+
+			delta.addOperation( operation );
+			this.document.applyOperation( operation );
+		};
+
+		if ( itemOrRange instanceof Range ) {
+			// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
+			const ranges = itemOrRange.getMinimalFlatRanges().reverse();
+
+			for ( const flat of ranges ) {
+				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
+			}
+		} else {
+			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
+
+			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
+		}
+	}
+
+	/**
+	 * 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.
+	 *
+	 * @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 );
+	}
+
+	/**
+	 * Renames given element.
+	 *
+	 * @param {module:engine/model/element~Element} element The element to rename.
+	 * @param {String} newName New element name.
+	 */
+	rename( element, newName ) {
+		if ( !( element instanceof Element ) ) {
+			/**
+			 * Trying to rename an object which is not an instance of Element.
+			 *
+			 * @error batch-rename-not-element-instance
+			 */
+			throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
+		}
+
+		const delta = new RenameDelta();
+		this.addDelta( delta );
+
+		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
+		delta.addOperation( renameOperation );
+		this.document.applyOperation( renameOperation );
+	}
+
+	/**
+	 * Splits an element at the given position.
+	 *
+	 * The element needs to have a parent. It cannot be a root element nor document fragment.
+	 * The `batch-split-element-no-parent` error will be thrown if you try to split an element with no parent.
+	 *
+	 * @param {module:engine/model/position~Position} position Position of split.
+	 */
+	split( position ) {
+		const delta = new SplitDelta();
+		this.addDelta( delta );
+
+		const splitElement = position.parent;
+
+		if ( !splitElement.parent ) {
+			/**
+			 * Element with no parent can not be split.
+			 *
+			 * @error batch-split-element-no-parent
+			 */
+			throw new CKEditorError( 'batch-split-element-no-parent: Element with no parent can not be split.' );
+		}
+
+		const copy = new Element( splitElement.name, splitElement.getAttributes() );
+
+		const insert = new InsertOperation(
+			Position.createAfter( splitElement ),
+			copy,
+			this.document.version
+		);
+
+		delta.addOperation( insert );
+		this.document.applyOperation( insert );
+
+		const move = new MoveOperation(
+			position,
+			splitElement.maxOffset - position.offset,
+			Position.createFromParentAndOffset( copy, 0 ),
+			this.document.version
+		);
+		move.isSticky = true;
+
+		delta.addOperation( move );
+		this.document.applyOperation( move );
+	}
+
+	/**
+	 * Wraps given range with given element or with a new element with specified name, if string has been passed.
+	 *
+	 * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
+	 *
+	 * @param {module:engine/model/range~Range} range Range to wrap.
+	 * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
+	 */
+	wrap( range, elementOrString ) {
+		if ( !range.isFlat ) {
+			/**
+			 * Range to wrap is not flat.
+			 *
+			 * @error batch-wrap-range-not-flat
+			 */
+			throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
+		}
+
+		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
+
+		if ( element.childCount > 0 ) {
+			/**
+			 * Element to wrap with is not empty.
+			 *
+			 * @error batch-wrap-element-not-empty
+			 */
+			throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
+		}
+
+		if ( element.parent !== null ) {
+			/**
+			 * Element to wrap with is already attached to a tree model.
+			 *
+			 * @error batch-wrap-element-attached
+			 */
+			throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
+		}
+
+		const delta = new WrapDelta();
+		this.addDelta( delta );
+
+		const insert = new InsertOperation( range.end, element, this.document.version );
+		delta.addOperation( insert );
+		this.document.applyOperation( insert );
+
+		const targetPosition = Position.createFromParentAndOffset( element, 0 );
+		const move = new MoveOperation(
+			range.start,
+			range.end.offset - range.start.offset,
+			targetPosition,
+			this.document.version
+		);
+		delta.addOperation( move );
+		this.document.applyOperation( move );
+	}
+
+	/**
+	 * Unwraps children of the given element – all its children are moved before it and then the element is removed.
+	 * Throws error if you try to unwrap an element which does not have a parent.
+	 *
+	 * @param {module:engine/model/element~Element} element Element to unwrap.
+	 */
+	unwrap( element ) {
+		if ( element.parent === null ) {
+			/**
+			 * Trying to unwrap an element which has no parent.
+			 *
+			 * @error batch-unwrap-element-no-parent
+			 */
+			throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
+		}
+
+		const delta = new UnwrapDelta();
+		this.addDelta( delta );
+
+		const sourcePosition = Position.createFromParentAndOffset( element, 0 );
+
+		const move = new MoveOperation(
+			sourcePosition,
+			element.maxOffset,
+			Position.createBefore( element ),
+			this.document.version
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.document.applyOperation( move );
+
+		// Computing new position because we moved some nodes before `element`.
+		// If we would cache `Position.createBefore( element )` we remove wrong node.
+		const graveyard = this.document.graveyard;
+		const gyPosition = new Position( graveyard, [ 0 ] );
+
+		const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
+		delta.addOperation( remove );
+		this.document.applyOperation( remove );
+	}
+
+	/**
+	 * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
+	 *
+	 * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
+	 * is passed), `range` parameter may be omitted. In this case marker will not be updated in
+	 * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
+	 * the document history. This may be important for other features, like undo. From document history point of view, it will
+	 * look like the marker was created and added to the document at the moment when it is set using this method.
+	 *
+	 * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
+	 * is waiting for additional data, etc.). In this case, the marker may be first created directly through
+	 * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
+	 *
+	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
+	 * @param {module:engine/model/range~Range} [newRange] Marker range.
+	 */
+	setMarker( markerOrName, newRange ) {
+		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+		const currentMarker = this.document.markers.get( name );
+
+		if ( !newRange && !currentMarker ) {
+			/**
+			 * Range parameter is required when adding a new marker.
+			 *
+			 * @error batch-setMarker-no-range
+			 */
+			throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
+		}
+
+		const currentRange = currentMarker ? currentMarker.getRange() : null;
+
+		if ( !newRange ) {
+			// If `newRange` is not given, treat this as synchronizing existing marker.
+			// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
+			addMarkerOperation( this, name, null, currentRange );
+		} else {
+			// Just change marker range.
+			addMarkerOperation( this, name, currentRange, newRange );
+		}
+	}
+
+	/**
+	 * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
+	 *
+	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
+	 */
+	removeMarker( markerOrName ) {
+		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+
+		if ( !this.document.markers.has( name ) ) {
+			/**
+			 * Trying to remove marker which does not exist.
+			 *
+			 * @error batch-removeMarker-no-marker
+			 */
+			throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
+		}
+
+		const oldRange = this.document.markers.get( name ).getRange();
+
+		addMarkerOperation( this, name, oldRange, null );
+	}
 }
 
-/**
- * Function to register batch methods. To make code scalable `Batch` do not have modification
- * methods built in. They can be registered using this method.
- *
- * This method checks if there is no naming collision and throws `batch-register-taken` if the method name
- * is already taken.
- *
- * Besides that no magic happens here, the method is added to the `Batch` class prototype.
- *
- * For example:
- *
- *		Batch.register( 'insert', function( position, nodes ) {
- *			// You can use a class inheriting from `Delta` if that class should handle OT in a special way.
- *			const delta = new Delta();
- *
- *			// Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation.
- *			this.addDelta( delta );
- *
- *			// Create operations which should be components of this delta.
- *			const operation = new InsertOperation( position, nodes, this.document.version );
- *
- *			// Add operation to the delta. It is important to add operation before applying it.
- *			delta.addOperation( operation );
- *
- *			// Remember to apply every operation, no magic, you need to do it manually.
- *			this.document.applyOperation( operation );
- *
- *			// Make this method chainable.
- *			return this;
- *		} );
- *
- * @method module:engine/model/batch~Batch.register
- * @param {String} name Method name.
- * @param {Function} creator Method body.
- */
-export function register( name, creator ) {
-	if ( Batch.prototype[ name ] ) {
-		/**
-		 * This batch method name is already taken.
-		 *
-		 * @error batch-register-taken
-		 * @param {String} name
-		 */
-		throw new CKEditorError(
-			'model-batch-register-taken: This batch method name is already taken.',
-			{ name } );
+// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
+//
+// Because attribute operation needs to have the same attribute value on the whole range, this function splits
+// the range into smaller parts.
+//
+// @private
+// @param {module:engine/model/batch~Batch} batch
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
+function setAttributeToRange( batch, key, value, range ) {
+	const delta = new AttributeDelta();
+	const doc = batch.document;
+
+	// Position of the last split, the beginning of the new range.
+	let lastSplitPosition = range.start;
+
+	// Currently position in the scanning range. Because we need value after the position, it is not a current
+	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
+	let position;
+
+	// Value before the currently position.
+	let valueBefore;
+
+	// Value after the currently position.
+	let valueAfter;
+
+	for ( const val of range ) {
+		valueAfter = val.item.getAttribute( key );
+
+		// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
+		// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
+		if ( position && valueBefore != valueAfter ) {
+			// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
+			if ( valueBefore != value ) {
+				addOperation();
+			}
+
+			lastSplitPosition = position;
+		}
+
+		position = val.nextPosition;
+		valueBefore = valueAfter;
+	}
+
+	// Because position in the loop is not the iterator position (see let position comment), the last position in
+	// the while loop will be last but one position in the range. We need to check the last position manually.
+	if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
+		addOperation();
+	}
+
+	function addOperation() {
+		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
+		if ( delta.operations.length === 0 ) {
+			batch.addDelta( delta );
+		}
+
+		const range = new Range( lastSplitPosition, position );
+		const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
+
+		delta.addOperation( operation );
+		doc.applyOperation( operation );
+	}
+}
+
+// Sets given attribute to the given node. When attribute value is null then attribute will be removed.
+//
+// @private
+// @param {module:engine/model/batch~Batch} batch
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
+function setAttributeToItem( batch, key, value, item ) {
+	const doc = batch.document;
+	const previousValue = item.getAttribute( key );
+	let range, operation;
+
+	if ( previousValue != value ) {
+		const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta();
+		batch.addDelta( delta );
+
+		if ( item.root === item ) {
+			// If we change attributes of root element, we have to use `RootAttributeOperation`.
+			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
+		} else {
+			if ( item.is( 'element' ) ) {
+				// If we change the attribute of the element, we do not want to change attributes of its children, so
+				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
+				// it's children, so the range will contain only the opening tag.
+				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
+			} else {
+				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
+				// all characters represented by it.
+				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
+			}
+
+			operation = new AttributeOperation( range, key, previousValue, value, doc.version );
+		}
+
+		delta.addOperation( operation );
+		doc.applyOperation( operation );
+	}
+}
+
+// Creates and adds marker operation to {@link module:engine/model/delta/delta~Delta delta}.
+//
+// @private
+// @param {module:engine/model/batch~Batch} batch
+// @param {String} name Marker name.
+// @param {module:engine/model/range~Range} oldRange Marker range before the change.
+// @param {module:engine/model/range~Range} newRange Marker range after the change.
+function addMarkerOperation( batch, name, oldRange, newRange ) {
+	const doc = batch.document;
+	const delta = new MarkerDelta();
+
+	const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
+
+	batch.addDelta( delta );
+	delta.addOperation( operation );
+	doc.applyOperation( operation );
+}
+
+// Returns `true` if both root elements are the same element or both are documents root elements.
+//
+// Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
+// within the same document fragment), but when element supposed to be moved from document fragment to the document, or
+// to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
+// collaboration may track changes on the document but ignore changes on detached fragments and should not get
+// unexpected `move` operation.
+function isSameTree( rootA, rootB ) {
+	// If it is the same root this is the same tree.
+	if ( rootA === rootB ) {
+		return true;
+	}
+
+	// If both roots are documents root it is operation within the document what we still treat as the same tree.
+	if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
+		return true;
 	}
 
-	Batch.prototype[ name ] = creator;
+	return false;
 }

+ 0 - 149
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -9,11 +9,7 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import AttributeOperation from '../operation/attributeoperation';
-import RootAttributeOperation from '../operation/rootattributeoperation';
 import NoOperation from '../operation/nooperation';
-import Position from '../position';
 import Range from '../range';
 
 /**
@@ -112,149 +108,4 @@ export default class AttributeDelta extends Delta {
 	}
 }
 
-/**
- * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
- * use `RootAttributeDelta` class which inherits from the `Delta` class and may
- * overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export class RootAttributeDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.RootAttributeDelta';
-	}
-}
-
-/**
- * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
- * or on a {@link module:engine/model/range~Range range}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#setAttribute
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range on which the attribute will be set.
- * @param {String} key Attribute key.
- * @param {*} value Attribute new value.
- */
-register( 'setAttribute', function( itemOrRange, key, value ) {
-	attribute( this, key, value, itemOrRange );
-
-	return this;
-} );
-
-/**
- * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
- * or from a {@link module:engine/model/range~Range range}.
- *
- * @chainable
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range from which the attribute will be removed.
- * @method module:engine/model/batch~Batch#removeAttribute
- * @param {String} key Attribute key.
- */
-register( 'removeAttribute', function( itemOrRange, key ) {
-	attribute( this, key, null, itemOrRange );
-
-	return this;
-} );
-
-function attribute( batch, key, value, itemOrRange ) {
-	if ( itemOrRange instanceof Range ) {
-		changeRange( batch, batch.document, key, value, itemOrRange );
-	} else {
-		changeItem( batch, batch.document, key, value, itemOrRange );
-	}
-}
-
-function changeItem( batch, doc, key, value, item ) {
-	const previousValue = item.getAttribute( key );
-	let range, operation;
-
-	const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta();
-
-	if ( previousValue != value ) {
-		batch.addDelta( delta );
-
-		if ( item.is( 'rootElement' ) ) {
-			// If we change attributes of root element, we have to use `RootAttributeOperation`.
-			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
-		} else {
-			if ( item.is( 'element' ) ) {
-				// If we change the attribute of the element, we do not want to change attributes of its children, so
-				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
-				// it's children, so the range will contain only the opening tag.
-				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
-			} else {
-				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
-				// all characters represented by it.
-				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
-			}
-
-			operation = new AttributeOperation( range, key, previousValue, value, doc.version );
-		}
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
-// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range
-// into smaller parts.
-function changeRange( batch, doc, attributeKey, attributeValue, range ) {
-	const delta = new AttributeDelta();
-
-	// Position of the last split, the beginning of the new range.
-	let lastSplitPosition = range.start;
-
-	// Currently position in the scanning range. Because we need value after the position, it is not a current
-	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
-	let position,
-		// Value before the currently position.
-		attributeValueBefore,
-		// Value after the currently position.
-		attributeValueAfter;
-
-	for ( const value of range ) {
-		attributeValueAfter = value.item.getAttribute( attributeKey );
-
-		// At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but
-		// because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ).
-		if ( position && attributeValueBefore != attributeValueAfter ) {
-			// if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different.
-			if ( attributeValueBefore != attributeValue ) {
-				addOperation();
-			}
-
-			lastSplitPosition = position;
-		}
-
-		position = value.nextPosition;
-		attributeValueBefore = attributeValueAfter;
-	}
-
-	// Because position in the loop is not the iterator position (see let position comment), the last position in
-	// the while loop will be last but one position in the range. We need to check the last position manually.
-	if ( position instanceof Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) {
-		addOperation();
-	}
-
-	function addOperation() {
-		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
-		if ( delta.operations.length === 0 ) {
-			batch.addDelta( delta );
-		}
-
-		const range = new Range( lastSplitPosition, position );
-		const operation = new AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version );
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
 DeltaFactory.register( AttributeDelta );
-DeltaFactory.register( RootAttributeDelta );

+ 0 - 1
packages/ckeditor5-engine/src/model/delta/basic-deltas.js

@@ -13,7 +13,6 @@
 
 // Import default suite of deltas so a feature have to include only Batch class file.
 import './attributedelta';
-import './insertdelta';
 import './mergedelta';
 import './movedelta';
 import './removedelta';

+ 0 - 50
packages/ckeditor5-engine/src/model/delta/insertdelta.js

@@ -10,13 +10,6 @@
 import Delta from './delta';
 import RemoveDelta from './removedelta';
 import DeltaFactory from './deltafactory';
-import InsertOperation from '../operation/insertoperation';
-import { register } from '../batch';
-import { normalizeNodes } from './../writer';
-
-import DocumentFragment from '../documentfragment';
-import Range from '../../model/range.js';
-import Position from '../../model/position.js';
 
 /**
  * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method
@@ -78,47 +71,4 @@ export default class InsertDelta extends Delta {
 	}
 }
 
-/**
- * Inserts a node or nodes at the given position.
- *
- * When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will
- * be set to {@link module:engine/model/document~Document#markers}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#insert
- * @param {module:engine/model/position~Position} position Position of insertion.
- * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
- */
-register( 'insert', function( position, nodes ) {
-	const normalizedNodes = normalizeNodes( nodes );
-
-	// If nothing is inserted do not create delta and operation.
-	if ( normalizedNodes.length === 0 ) {
-		return this;
-	}
-
-	const delta = new InsertDelta();
-	const insert = new InsertOperation( position, normalizedNodes, this.document.version );
-
-	this.addDelta( delta );
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	// When element is a DocumentFragment we need to move its markers to Document#markers.
-	if ( nodes instanceof DocumentFragment ) {
-		for ( const [ markerName, markerRange ] of nodes.markers ) {
-			// We need to migrate marker range from DocumentFragment to Document.
-			const rangeRootPosition = Position.createAt( markerRange.root );
-			const range = new Range(
-				markerRange.start._getCombined( rangeRootPosition, position ),
-				markerRange.end._getCombined( rangeRootPosition, position )
-			);
-
-			this.setMarker( markerName, range );
-		}
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( InsertDelta );

+ 0 - 85
packages/ckeditor5-engine/src/model/delta/markerdelta.js

@@ -9,9 +9,6 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import MarkerOperation from '../operation/markeroperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#setMarker Batch#setMarker}
@@ -46,86 +43,4 @@ export default class MarkerDelta extends Delta {
 	}
 }
 
-/**
- * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
- *
- * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
- * is passed), `range` parameter may be omitted. In this case marker will not be updated in
- * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
- * the document history. This may be important for other features, like undo. From document history point of view, it will
- * look like the marker was created and added to the document at the moment when it is set using this method.
- *
- * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
- * is waiting for additional data, etc.). In this case, the marker may be first created directly through
- * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#setMarker
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
- * @param {module:engine/model/range~Range} [newRange] Marker range.
- */
-register( 'setMarker', function( markerOrName, newRange ) {
-	const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-	const currentMarker = this.document.markers.get( name );
-
-	if ( !newRange && !currentMarker ) {
-		/**
-		 * Range parameter is required when adding a new marker.
-		 *
-		 * @error batch-setMarker-no-range
-		 */
-		throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
-	}
-
-	const currentRange = currentMarker ? currentMarker.getRange() : null;
-
-	if ( !newRange ) {
-		// If `newRange` is not given, treat this as synchronizing existing marker.
-		// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
-		addOperation( this, name, null, currentRange );
-	} else {
-		// Just change marker range.
-		addOperation( this, name, currentRange, newRange );
-	}
-
-	return this;
-} );
-
-/**
- * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#removeMarker
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
- */
-register( 'removeMarker', function( markerOrName ) {
-	const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-
-	if ( !this.document.markers.has( name ) ) {
-		/**
-		 * Trying to remove marker which does not exist.
-		 *
-		 * @error batch-removeMarker-no-marker
-		 */
-		throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
-	}
-
-	const oldRange = this.document.markers.get( name ).getRange();
-
-	addOperation( this, name, oldRange, null );
-
-	return this;
-} );
-
-function addOperation( batch, name, oldRange, newRange ) {
-	const doc = batch.document;
-	const delta = new MarkerDelta();
-
-	const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
-
-	batch.addDelta( delta );
-	delta.addOperation( operation );
-	doc.applyOperation( operation );
-}
-
 DeltaFactory.register( MarkerDelta );

+ 0 - 65
packages/ckeditor5-engine/src/model/delta/mergedelta.js

@@ -10,12 +10,6 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import SplitDelta from './splitdelta';
-import { register } from '../batch';
-import Position from '../position';
-import Element from '../element';
-import RemoveOperation from '../operation/removeoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
@@ -70,63 +64,4 @@ export default class MergeDelta extends Delta {
 	}
 }
 
-/**
- * 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
- * @method module:engine/model/batch~Batch#merge
- * @param {module:engine/model/position~Position} position Position of merge.
- */
-register( 'merge', function( 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;
-} );
-
 DeltaFactory.register( MergeDelta );

+ 0 - 41
packages/ckeditor5-engine/src/model/delta/movedelta.js

@@ -9,11 +9,6 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import MoveOperation from '../operation/moveoperation';
-import Position from '../position';
-import Range from '../range';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method
@@ -86,40 +81,4 @@ export default class MoveDelta extends Delta {
 	}
 }
 
-function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) {
-	const operation = new MoveOperation( sourcePosition, howMany, targetPosition, batch.document.version );
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Moves given {@link module:engine/model/item~Item model item} or given range to target position.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#move
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move.
- * @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted.
- */
-register( 'move', function( itemOrRange, targetPosition ) {
-	const delta = new MoveDelta();
-	this.addDelta( delta );
-
-	if ( itemOrRange instanceof Range ) {
-		if ( !itemOrRange.isFlat ) {
-			/**
-			 * Range to move is not flat.
-			 *
-			 * @error batch-move-range-not-flat
-			 */
-			throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
-		}
-
-		addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition );
-	} else {
-		addMoveOperation( this, delta, Position.createBefore( itemOrRange ), 1, targetPosition );
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( MoveDelta );

+ 0 - 38
packages/ckeditor5-engine/src/model/delta/removedelta.js

@@ -8,11 +8,7 @@
  */
 
 import MoveDelta from './movedelta';
-import { register } from '../batch';
 import DeltaFactory from './deltafactory';
-import RemoveOperation from '../operation/removeoperation';
-import Position from '../position';
-import Range from '../range';
 
 /**
  * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method
@@ -29,38 +25,4 @@ export default class RemoveDelta extends MoveDelta {
 	}
 }
 
-function addRemoveDelta( batch, position, howMany ) {
-	const delta = new RemoveDelta();
-	batch.addDelta( delta );
-
-	const graveyard = batch.document.graveyard;
-	const gyPosition = new Position( graveyard, [ 0 ] );
-
-	const operation = new RemoveOperation( position, howMany, gyPosition, batch.document.version );
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Removes given {@link module:engine/model/item~Item model item} or given range.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#remove
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
- */
-register( 'remove', function( itemOrRange ) {
-	if ( itemOrRange instanceof Range ) {
-		// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
-		const ranges = itemOrRange.getMinimalFlatRanges().reverse();
-
-		for ( const flat of ranges ) {
-			addRemoveDelta( this, flat.start, flat.end.offset - flat.start.offset );
-		}
-	} else {
-		addRemoveDelta( this, Position.createBefore( itemOrRange ), 1 );
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( RemoveDelta );

+ 0 - 37
packages/ckeditor5-engine/src/model/delta/renamedelta.js

@@ -9,11 +9,6 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import RenameOperation from '../operation/renameoperation';
-import Element from '../element';
-import Position from '../position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method
@@ -44,36 +39,4 @@ export default class RenameDelta extends Delta {
 	}
 }
 
-function apply( batch, delta, operation ) {
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Renames given element.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#rename
- * @param {module:engine/model/element~Element} element The element to rename.
- * @param {String} newName New element name.
- */
-register( 'rename', function( element, newName ) {
-	if ( !( element instanceof Element ) ) {
-		/**
-		 * Trying to rename an object which is not an instance of Element.
-		 *
-		 * @error batch-rename-not-element-instance
-		 */
-		throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
-	}
-
-	const delta = new RenameDelta();
-	this.addDelta( delta );
-
-	const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
-	apply( this, delta, renameOperation );
-
-	return this;
-} );
-
 DeltaFactory.register( RenameDelta );

+ 30 - 0
packages/ckeditor5-engine/src/model/delta/rootattributedelta.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/delta/rootattributedelta
+ */
+
+import Delta from './delta';
+import DeltaFactory from './deltafactory';
+
+/**
+ * To provide specific OT behavior and better collisions solving, methods to change attributes
+ * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
+ * use `RootAttributeDelta` class which inherits from the `Delta` class and may
+ * overwrite some methods.
+ *
+ * @extends module:engine/model/delta/delta~Delta
+ */
+export default class RootAttributeDelta extends Delta {
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.delta.RootAttributeDelta';
+	}
+}
+
+DeltaFactory.register( RootAttributeDelta );

+ 0 - 55
packages/ckeditor5-engine/src/model/delta/splitdelta.js

@@ -9,12 +9,7 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import Position from '../position';
-import Element from '../element';
-import InsertOperation from '../operation/insertoperation';
 import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import MergeDelta from '../delta/mergedelta';
 
 /**
@@ -85,54 +80,4 @@ export default class SplitDelta extends Delta {
 	}
 }
 
-/**
- * Splits an element at the given position.
- *
- * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if
- * you try to split the root element.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#split
- * @param {module:engine/model/position~Position} position Position of split.
- */
-register( 'split', function( position ) {
-	const delta = new SplitDelta();
-	this.addDelta( delta );
-
-	const splitElement = position.parent;
-
-	if ( !splitElement.parent ) {
-		/**
-		 * Root element can not be split.
-		 *
-		 * @error batch-split-root
-		 */
-		throw new CKEditorError( 'batch-split-root: Root element can not be split.' );
-	}
-
-	const copy = new Element( splitElement.name, splitElement.getAttributes() );
-
-	const insert = new InsertOperation(
-		Position.createAfter( splitElement ),
-		copy,
-		this.document.version
-	);
-
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	const move = new MoveOperation(
-		position,
-		splitElement.maxOffset - position.offset,
-		Position.createFromParentAndOffset( copy, 0 ),
-		this.document.version
-	);
-	move.isSticky = true;
-
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	return this;
-} );
-
 DeltaFactory.register( SplitDelta );

+ 0 - 51
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -10,11 +10,6 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import WrapDelta from './wrapdelta';
-import { register } from '../batch';
-import Position from '../position';
-import RemoveOperation from '../operation/removeoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
@@ -64,50 +59,4 @@ export default class UnwrapDelta extends Delta {
 	}
 }
 
-/**
- * Unwraps children of the given element – all its children are moved before it and then the element is removed.
- * Throws error if you try to unwrap an element which does not have a parent.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#unwrap
- * @param {module:engine/model/element~Element} position Element to unwrap.
- */
-register( 'unwrap', function( element ) {
-	if ( element.parent === null ) {
-		/**
-		 * Trying to unwrap an element which has no parent.
-		 *
-		 * @error batch-unwrap-element-no-parent
-		 */
-		throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
-	}
-
-	const delta = new UnwrapDelta();
-	this.addDelta( delta );
-
-	const sourcePosition = Position.createFromParentAndOffset( element, 0 );
-
-	const move = new MoveOperation(
-		sourcePosition,
-		element.maxOffset,
-		Position.createBefore( element ),
-		this.document.version
-	);
-
-	move.isSticky = true;
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	// Computing new position because we moved some nodes before `element`.
-	// If we would cache `Position.createBefore( element )` we remove wrong node.
-	const graveyard = this.document.graveyard;
-	const gyPosition = new Position( graveyard, [ 0 ] );
-
-	const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
-	delta.addOperation( remove );
-	this.document.applyOperation( remove );
-
-	return this;
-} );
-
 DeltaFactory.register( UnwrapDelta );

+ 0 - 40
packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js

@@ -8,10 +8,7 @@
  */
 
 import InsertDelta from './insertdelta';
-import { register } from '../batch';
 import DeltaFactory from './deltafactory';
-import InsertOperation from '../operation/insertoperation';
-import { normalizeNodes } from './../writer';
 
 /**
  * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method
@@ -28,41 +25,4 @@ export default class WeakInsertDelta extends InsertDelta {
 	}
 }
 
-/**
- * Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions
- * like typing or plain-text paste (without formatting). There are two differences between
- * {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}:
- *
- * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of
- * {@link module:engine/model/document~Document#selection document selection}.
- * * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by
- * {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation},
- * the attribute operation is split into two operations.
- * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that
- * {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also
- * applies attributes for inserted nodes. This behavior has to be reflected during
- * {@link module:engine/model/delta/transform~transform delta transformation}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#weakInsert
- * @param {module:engine/model/position~Position} position Position of insertion.
- * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
- */
-register( 'weakInsert', function( position, nodes ) {
-	const delta = new WeakInsertDelta();
-	this.addDelta( delta );
-
-	nodes = normalizeNodes( nodes );
-
-	for ( const node of nodes ) {
-		node.setAttributesTo( this.document.selection.getAttributes() );
-	}
-
-	const operation = new InsertOperation( position, nodes, this.document.version );
-	delta.addOperation( operation );
-	this.document.applyOperation( operation );
-
-	return this;
-} );
-
 DeltaFactory.register( WeakInsertDelta );

+ 0 - 65
packages/ckeditor5-engine/src/model/delta/wrapdelta.js

@@ -10,13 +10,7 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import UnwrapDelta from './unwrapdelta';
-import { register } from '../batch';
-import Position from '../position';
 import Range from '../range';
-import Element from '../element';
-import InsertOperation from '../operation/insertoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
@@ -91,63 +85,4 @@ export default class WrapDelta extends Delta {
 	}
 }
 
-/**
- * Wraps given range with given element or with a new element with specified name, if string has been passed.
- * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#wrap
- * @param {module:engine/model/range~Range} range Range to wrap.
- * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
- */
-register( 'wrap', function( range, elementOrString ) {
-	if ( !range.isFlat ) {
-		/**
-		 * Range to wrap is not flat.
-		 *
-		 * @error batch-wrap-range-not-flat
-		 */
-		throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
-	}
-
-	const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
-
-	if ( element.childCount > 0 ) {
-		/**
-		 * Element to wrap with is not empty.
-		 *
-		 * @error batch-wrap-element-not-empty
-		 */
-		throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
-	}
-
-	if ( element.parent !== null ) {
-		/**
-		 * Element to wrap with is already attached to a tree model.
-		 *
-		 * @error batch-wrap-element-attached
-		 */
-		throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
-	}
-
-	const delta = new WrapDelta();
-	this.addDelta( delta );
-
-	const insert = new InsertOperation( range.end, element, this.document.version );
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	const targetPosition = Position.createFromParentAndOffset( element, 0 );
-	const move = new MoveOperation(
-		range.start,
-		range.end.offset - range.start.offset,
-		targetPosition,
-		this.document.version
-	);
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	return this;
-} );
-
 DeltaFactory.register( WrapDelta );

+ 9 - 7
packages/ckeditor5-engine/src/model/document.js

@@ -37,7 +37,9 @@ const graveyardName = '$graveyard';
  * All changes in the document are done by {@link module:engine/model/operation/operation~Operation operations}. To create operations in
  * a simple way, use the {@link module:engine/model/batch~Batch} API, for example:
  *
- *		doc.batch().insert( position, nodes ).split( otherPosition );
+ *		const batch = doc.batch();
+ *		batch.insert( node, position );
+ *		batch.split( otherPosition );
  *
  * @see module:engine/model/document~Document#batch
  * @mixes module:utils/emittermixin~EmitterMixin
@@ -163,11 +165,11 @@ export default class Document {
 
 		const changes = operation._execute();
 
-		this.version++;
-
-		this.history.addDelta( operation.delta );
-
-		this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
+		if ( operation.isDocumentOperation ) {
+			this.version++;
+			this.history.addDelta( operation.delta );
+			this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
+		}
 	}
 
 	/**
@@ -419,7 +421,7 @@ export default class Document {
 	/**
 	 * Fired when document changes by applying an operation.
 	 *
-	 * There are 5 types of change:
+	 * There are a few types of change:
 	 *
 	 * * 'insert' when nodes are inserted,
 	 * * 'remove' when nodes are removed,

+ 5 - 5
packages/ckeditor5-engine/src/model/documentselection.js

@@ -543,7 +543,7 @@ export default class DocumentSelection extends Selection {
 	_removeStoredAttribute( key ) {
 		const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-		this._document.batch().removeAttribute( this.anchor.parent, storeKey );
+		this._document.batch().removeAttribute( storeKey, this.anchor.parent );
 	}
 
 	/**
@@ -556,7 +556,7 @@ export default class DocumentSelection extends Selection {
 	_storeAttribute( key, value ) {
 		const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-		this._document.batch().setAttribute( this.anchor.parent, storeKey, value );
+		this._document.batch().setAttribute( storeKey, value, this.anchor.parent );
 	}
 
 	/**
@@ -572,13 +572,13 @@ export default class DocumentSelection extends Selection {
 		for ( const [ oldKey ] of this._getStoredAttributes() ) {
 			const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
 
-			batch.removeAttribute( selectionParent, storeKey );
+			batch.removeAttribute( storeKey, selectionParent );
 		}
 
 		for ( const [ key, value ] of attrs ) {
 			const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-			batch.setAttribute( selectionParent, storeKey, value );
+			batch.setAttribute( storeKey, value, selectionParent );
 		}
 	}
 
@@ -731,7 +731,7 @@ function clearAttributesStoredInElement( changes, batch, document ) {
 		const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
 
 		for ( const key of storedAttributes ) {
-			batch.removeAttribute( changeParent, key );
+			batch.removeAttribute( key, changeParent );
 		}
 	} );
 }

+ 5 - 0
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -73,6 +73,11 @@ export default class AttributeOperation extends Operation {
 		 * @member {*}
 		 */
 		this.newValue = newValue === undefined ? null : newValue;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = !!this.range.root.document;
 	}
 
 	/**

+ 87 - 0
packages/ckeditor5-engine/src/model/operation/detachoperation.js

@@ -0,0 +1,87 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/detachoperation
+ */
+
+import Operation from './operation';
+import Position from '../position';
+import Range from '../range';
+import { remove } from '../writer';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to permanently remove node from detached root.
+ * Note this operation is only a local operation and won't be send to the other clients.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class DetachOperation extends Operation {
+	/**
+	 * Creates an insert operation.
+	 *
+	 * @param {module:engine/model/position~Position} sourcePosition
+	 * Position before the first {@link module:engine/model/item~Item model item} to move.
+	 * @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
+	 * `sourcePosition` with offset shifted by `howMany`.
+	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
+	 */
+	constructor( sourcePosition, howMany, baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * Position before the first {@link module:engine/model/item~Item model item} to detach.
+		 *
+		 * @member {module:engine/model/position~Position} #sourcePosition
+		 */
+		this.sourcePosition = Position.createFromPosition( sourcePosition );
+
+		/**
+		 * Offset size of moved range.
+		 *
+		 * @member {Number} #howMany
+		 */
+		this.howMany = howMany;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = false;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'detach';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		if ( this.sourcePosition.root.document ) {
+			/**
+			 * Cannot detach document node.
+			 * Use {@link module:engine/model/operation/removeoperation~RemoveOperation remove operation} instead.
+			 *
+			 * @error detach-operation-on-document-node
+			 */
+			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' );
+		}
+
+		const nodes = remove( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ) );
+
+		return { nodes };
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.DetachOperation';
+	}
+}

+ 5 - 0
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -46,6 +46,11 @@ export default class InsertOperation extends Operation {
 		 * @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
 		 */
 		this.nodes = new NodeList( normalizeNodes( nodes ) );
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = !!this.position.root.document;
 	}
 
 	/**

+ 23 - 0
packages/ckeditor5-engine/src/model/operation/markeroperation.js

@@ -55,6 +55,11 @@ export default class MarkerOperation extends Operation {
 		 * @member {module:engine/model/markercollection~MarkerCollection}
 		 */
 		this._markers = markers;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = this._isDocumentOperation();
 	}
 
 	/**
@@ -65,6 +70,24 @@ export default class MarkerOperation extends Operation {
 	}
 
 	/**
+	 * Checks if operation is executed on document or document fragment nodes.
+	 *
+	 * @private
+	 */
+	_isDocumentOperation() {
+		if ( this.newRange ) {
+			return !!this.newRange.root.document;
+		}
+
+		if ( this.oldRange ) {
+			return !!this.oldRange.root.document;
+		}
+
+		// This is edge and might happen only on data from the server.
+		return true;
+	}
+
+	/**
 	 * Creates and returns an operation that has the same parameters as this operation.
 	 *
 	 * @returns {module:engine/model/operation/markeroperation~MarkerOperation} Clone of this operation.

+ 11 - 0
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -64,6 +64,17 @@ export default class MoveOperation extends Operation {
 		 * @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky
 		 */
 		this.isSticky = false;
+
+		/**
+		 * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
+		 *
+		 * Note that range cannot be moved within different documents e.g. from docFrag to document root so
+		 * root of source and target positions is always the same.
+		 *
+		 * @readonly
+		 * @member {Boolean} #isDocumentOperation
+		 */
+		this.isDocumentOperation = !!this.targetPosition.root.document;
 	}
 
 	/**

+ 12 - 0
packages/ckeditor5-engine/src/model/operation/nooperation.js

@@ -20,6 +20,18 @@ import Operation from './operation';
  * @extends module:engine/model/operation/operation~Operation
  */
 export default class NoOperation extends Operation {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = true;
+	}
+
 	get type() {
 		return 'noop';
 	}

+ 10 - 0
packages/ckeditor5-engine/src/model/operation/operation.js

@@ -46,6 +46,13 @@ export default class Operation {
 		 */
 
 		/**
+		 * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
+		 *
+		 * @readonly
+		 * @member {Boolean} #isDocumentOperation
+		 */
+
+		/**
 		 * Creates and returns an operation that has the same parameters as this operation.
 		 *
 		 * @method #clone
@@ -90,6 +97,9 @@ export default class Operation {
 		// Remove parent delta to avoid circular dependencies.
 		delete json.delta;
 
+		// Only document operations are shared with other clients so it is not necessary to keep this information.
+		delete json.isDocumentOperation;
+
 		return json;
 	}
 

+ 31 - 0
packages/ckeditor5-engine/src/model/operation/reinsertoperation.js

@@ -9,6 +9,7 @@
 
 import MoveOperation from './moveoperation';
 import RemoveOperation from './removeoperation';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like
@@ -18,6 +19,21 @@ import RemoveOperation from './removeoperation';
  */
 export default class ReinsertOperation extends MoveOperation {
 	/**
+	 * @inheritDocs
+	 */
+	constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
+		super( sourcePosition, howMany, targetPosition, baseVersion );
+
+		/**
+		 * Reinsert operation is always executed on attached items.
+		 *
+		 * @readonly
+		 * @member {Boolean}
+		 */
+		this.isDocumentOperation = true;
+	}
+
+	/**
 	 * Position where nodes will be re-inserted.
 	 *
 	 * @type {module:engine/model/position~Position}
@@ -54,6 +70,21 @@ export default class ReinsertOperation extends MoveOperation {
 	/**
 	 * @inheritDoc
 	 */
+	_execute() {
+		if ( !this.sourcePosition.root.document ) {
+			throw new CKEditorError( 'reinsert-operation-on-detached-item: Cannot reinsert detached item.' );
+		}
+
+		if ( !this.targetPosition.root.document ) {
+			throw new CKEditorError( 'reinsert-operation-to-detached-parent: Cannot reinsert item to detached parent.' );
+		}
+
+		return super._execute();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	static get className() {
 		return 'engine.model.operation.ReinsertOperation';
 	}

+ 35 - 0
packages/ckeditor5-engine/src/model/operation/removeoperation.js

@@ -9,12 +9,29 @@
 
 import MoveOperation from './moveoperation';
 import ReinsertOperation from './reinsertoperation';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to remove a range of nodes.
  */
 export default class RemoveOperation extends MoveOperation {
 	/**
+	 * @inheritDocs
+	 */
+	constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
+		super( sourcePosition, howMany, targetPosition, baseVersion );
+
+		/**
+		 * Remove operation cannot be applied on element that is not inside the document
+		 * so this will always be a document operation.
+		 *
+		 * @readonly
+		 * @member {Boolean}
+		 */
+		this.isDocumentOperation = true;
+	}
+
+	/**
 	 * @inheritDoc
 	 */
 	get type() {
@@ -35,6 +52,24 @@ export default class RemoveOperation extends MoveOperation {
 	/**
 	 * @inheritDoc
 	 */
+	_execute() {
+		if ( !this.sourcePosition.root.document ) {
+			/**
+			 * Item that is going to be removed needs to be a {@link module:engine/model/document~Document document} child.
+			 * To remove Item from detached document fragment use
+			 * {@link module:engine/model/operation/detachoperation~DetachOperation DetachOperation}.
+			 *
+			 * @error remove-operation-on-detached-item
+			 */
+			throw new CKEditorError( 'remove-operation-on-detached-item: Cannot remove detached item.' );
+		}
+
+		return super._execute();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	static get className() {
 		return 'engine.model.operation.RemoveOperation';
 	}

+ 5 - 0
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -51,6 +51,11 @@ export default class RenameOperation extends Operation {
 		 * @member {String} module:engine/model/operation/renameoperation~RenameOperation#newName
 		 */
 		this.newName = newName;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = !!this.position.root.document;
 	}
 
 	/**

+ 8 - 0
packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js

@@ -67,8 +67,16 @@ export default class RootAttributeOperation extends Operation {
 		 * @member {*}
 		 */
 		this.newValue = newValue;
+
+		/**
+		 * @inheritDoc
+		 */
+		this.isDocumentOperation = !!this.root.document;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		if ( this.oldValue === null ) {
 			return 'addRootAttribute';

+ 3 - 10
packages/ckeditor5-engine/src/model/schema.js

@@ -414,14 +414,11 @@ export default class Schema {
 	}
 
 	/**
-	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes.
-	 * When {@link module:engine/model/batch~Batch batch} parameter is provided then attributes will be removed
-	 * using that batch, by creating {@link module:engine/model/delta/attributedelta~AttributeDelta attribute deltas}.
-	 * Otherwise, attributes will be removed directly from provided nodes using {@link module:engine/model/node~Node node} API.
+	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes..
 	 *
 	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
 	 * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
-	 * @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 */
 	removeDisallowedAttributes( nodes, inside, batch ) {
 		for ( const node of nodes ) {
@@ -435,11 +432,7 @@ export default class Schema {
 				// TODO: this should be improved to check all combination of attributes.
 				for ( const attribute of node.getAttributeKeys() ) {
 					if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) {
-						if ( batch ) {
-							batch.removeAttribute( node, attribute );
-						} else {
-							node.removeAttribute( attribute );
-						}
+						batch.removeAttribute( attribute, node );
 					}
 				}
 			}

+ 22 - 14
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -48,7 +48,7 @@ describe( 'DataController', () => {
 	describe( 'parse', () => {
 		it( 'should set text', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( 'foobar' );
@@ -59,7 +59,7 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foobar</paragraph>' );
@@ -70,7 +70,7 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo</p><p>bar</p>' );
+			const model = data.parse( '<p>foo</p><p>bar</p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
@@ -83,20 +83,20 @@ describe( 'DataController', () => {
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 			buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', true );
 
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 		} );
 
 		it( 'should parse in the root context by default', () => {
-			const model = data.parse( 'foo' );
+			const model = data.parse( 'foo', modelDocument.batch() );
 
 			expect( stringify( model ) ).to.equal( '' );
 		} );
 
 		it( 'should accept parsing context', () => {
-			const model = data.parse( 'foo', '$block' );
+			const model = data.parse( 'foo', modelDocument.batch(), '$block' );
 
 			expect( stringify( model ) ).to.equal( 'foo' );
 		} );
@@ -111,7 +111,7 @@ describe( 'DataController', () => {
 
 		it( 'should convert content of an element #1', () => {
 			const viewElement = parseView( '<p>foo</p>' );
-			const model = data.toModel( viewElement );
+			const model = data.toModel( viewElement, modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph>' );
@@ -119,7 +119,7 @@ describe( 'DataController', () => {
 
 		it( 'should convert content of an element #2', () => {
 			const viewFragment = parseView( '<p>foo</p><p>bar</p>' );
-			const model = data.toModel( viewFragment );
+			const model = data.toModel( viewFragment, modelDocument.batch() );
 
 			expect( model ).to.be.instanceOf( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
@@ -134,10 +134,10 @@ describe( 'DataController', () => {
 			const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] );
 
 			// Model fragment in root.
-			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' );
+			expect( stringify( data.toModel( viewFragment, modelDocument.batch() ) ) ).to.equal( '' );
 
 			// Model fragment in inline root.
-			expect( stringify( data.toModel( viewFragment, 'inlineRoot' ) ) ).to.equal( 'foo' );
+			expect( stringify( data.toModel( viewFragment, modelDocument.batch(), 'inlineRoot' ) ) ).to.equal( 'foo' );
 		} );
 	} );
 
@@ -264,6 +264,8 @@ describe( 'DataController', () => {
 	} );
 
 	describe( 'stringify', () => {
+		let batch;
+
 		beforeEach( () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'div' );
@@ -272,16 +274,18 @@ describe( 'DataController', () => {
 			modelDocument.schema.allow( { name: 'div', inside: '$root' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+
+			batch = modelDocument.batch();
 		} );
 
 		it( 'should stringify a content of an element', () => {
-			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema, batch );
 
 			expect( data.stringify( modelElement ) ).to.equal( '<p>foo</p>' );
 		} );
 
 		it( 'should stringify a content of a document fragment', () => {
-			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema, batch );
 
 			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
@@ -299,7 +303,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should convert a content of an element', () => {
-			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema, modelDocument.batch() );
 
 			const viewDocumentFragment = data.toView( modelElement );
 
@@ -313,7 +317,11 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should convert a document fragment', () => {
-			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+			const modelDocumentFragment = parseModel(
+				'<paragraph>foo</paragraph><paragraph>bar</paragraph>',
+				modelDocument.schema,
+				modelDocument.batch()
+			);
 
 			const viewDocumentFragment = data.toView( modelDocumentFragment );
 

+ 8 - 5
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -157,11 +157,12 @@ describe( 'EditingController', () => {
 				'<paragraph>foo</paragraph>' +
 				'<paragraph></paragraph>' +
 				'<paragraph>bar</paragraph>',
-				model.schema
+				model.schema,
+				model.batch()
 			)._children );
 
 			model.enqueueChanges( () => {
-				model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData );
+				model.batch().insert( modelData, model.getRoot() );
 				model.selection.addRange( ModelRange.createFromParentsAndOffsets(
 					modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 ) );
 			} );
@@ -380,7 +381,7 @@ describe( 'EditingController', () => {
 
 		it( 'should forward add marker event if content is moved into a marker range', () => {
 			model.enqueueChanges( () => {
-				model.batch().insert( ModelPosition.createAt( model.getRoot(), 'end' ), new ModelElement( 'paragraph' ) );
+				model.batch().appendElement( 'paragraph', model.getRoot() );
 			} );
 
 			const markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
@@ -414,9 +415,11 @@ describe( 'EditingController', () => {
 
 			editing.destroy();
 
+			const batch = model.batch();
+
 			model.enqueueChanges( () => {
-				const modelData = parse( '<paragraph>foo</paragraph>', model.schema ).getChild( 0 );
-				model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData );
+				const modelData = parse( '<paragraph>foo</paragraph>', model.schema, batch ).getChild( 0 );
+				batch.insert( modelData, model.getRoot() );
 			} );
 
 			expect( spy.called ).to.be.false;

+ 4 - 2
packages/ckeditor5-engine/tests/controller/insertcontent.js

@@ -756,12 +756,14 @@ describe( 'DataController', () => {
 	//
 	// @param {module:engine/model/item~Item|String} content
 	function insertHelper( content ) {
+		const batch = doc.batch();
+
 		if ( typeof content == 'string' ) {
-			content = parse( content, doc.schema, {
+			content = parse( content, doc.schema, batch, {
 				context: [ '$clipboardHolder' ]
 			} );
 		}
 
-		insertContent( dataController, content, doc.selection );
+		insertContent( dataController, content, doc.selection, batch );
 	}
 } );

+ 20 - 19
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -40,12 +40,13 @@ import { convertToModelFragment, convertText } from '../../src/conversion/view-t
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
 
 describe( 'advanced-converters', () => {
-	let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher;
+	let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher, batch;
 
 	beforeEach( () => {
 		modelDoc = new ModelDocument();
 		modelRoot = modelDoc.createRoot();
 		viewRoot = new ViewContainerElement( 'div' );
+		batch = modelDoc.batch();
 
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
@@ -207,8 +208,8 @@ describe( 'advanced-converters', () => {
 
 			const viewFigureConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
-					const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable );
-					const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable );
+					const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable, batch );
+					const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable, batch );
 
 					modelImage.appendChildren( modelCaption );
 
@@ -231,7 +232,7 @@ describe( 'advanced-converters', () => {
 			const viewFigcaptionConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
 					const modelCaption = new ModelElement( 'caption' );
-					const children = conversionApi.convertChildren( data.input, consumable );
+					const children = conversionApi.convertChildren( data.input, consumable, batch );
 
 					modelCaption.appendChildren( children );
 
@@ -286,7 +287,7 @@ describe( 'advanced-converters', () => {
 
 		it( 'should convert view image to model', () => {
 			const viewElement = new ViewContainerElement( 'img', { src: 'bar.jpg', title: 'bar' } );
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, batch );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"></image>' );
 		} );
@@ -300,7 +301,7 @@ describe( 'advanced-converters', () => {
 					new ViewContainerElement( 'figcaption', null, new ViewText( 'foobar' ) )
 				]
 			);
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, batch );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"><caption>foobar</caption></image>' );
 		} );
@@ -371,7 +372,7 @@ describe( 'advanced-converters', () => {
 			viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
 				if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) {
 					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
+						data.output = conversionApi.convertChildren( data.input, consumable, batch );
 					}
 
 					for ( const child of data.output ) {
@@ -383,7 +384,7 @@ describe( 'advanced-converters', () => {
 			viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
 				if ( consumable.consume( data.input, { attribute: 'title' } ) ) {
 					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
+						data.output = conversionApi.convertChildren( data.input, consumable, batch );
 					}
 
 					for ( const child of data.output ) {
@@ -468,7 +469,7 @@ describe( 'advanced-converters', () => {
 						}
 					}
 
-					const children = conversionApi.convertChildren( data.input, consumable );
+					const children = conversionApi.convertChildren( data.input, consumable, batch );
 					data.output.appendChildren( children );
 				}
 			} );
@@ -519,7 +520,7 @@ describe( 'advanced-converters', () => {
 		it( 'should convert a view element to model', () => {
 			const viewElement = new ViewAttributeElement( 'a', { href: 'foo.html', title: 'Foo title' }, new ViewText( 'foo' ) );
 
-			const modelText = viewDispatcher.convert( viewElement ).getChild( 0 );
+			const modelText = viewDispatcher.convert( viewElement, batch ).getChild( 0 );
 
 			expect( modelText ).to.be.instanceof( ModelText );
 			expect( modelText.data ).to.equal( 'foo' );
@@ -590,7 +591,7 @@ describe( 'advanced-converters', () => {
 				]
 			);
 
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, batch );
 
 			expect( modelToString( modelElement ) ).to.equal( '<quote linkHref="foo.html" linkTitle="Foo source">foo</quote>' );
 		} );
@@ -602,7 +603,7 @@ describe( 'advanced-converters', () => {
 		viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) {
 				if ( !data.output ) {
-					data.output = conversionApi.convertChildren( data.input, consumable );
+					data.output = conversionApi.convertChildren( data.input, consumable, batch );
 				}
 
 				for ( const child of data.output ) {
@@ -615,7 +616,7 @@ describe( 'advanced-converters', () => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
 				data.output = new ModelElement( 'paragraph' );
 
-				const children = conversionApi.convertChildren( data.input, consumable );
+				const children = conversionApi.convertChildren( data.input, consumable, batch );
 
 				for ( let i = 1; i < children.childCount; i++ ) {
 					const child = children.getChild( i );
@@ -632,13 +633,13 @@ describe( 'advanced-converters', () => {
 
 		viewDispatcher.on( 'element:table', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
+				data.output = conversionApi.convertChildren( data.input, consumable, batch );
 			}
 		} );
 
 		viewDispatcher.on( 'element:td', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
+				data.output = conversionApi.convertChildren( data.input, consumable, batch );
 			}
 		} );
 
@@ -653,7 +654,7 @@ describe( 'advanced-converters', () => {
 			] )
 		] );
 
-		const model = viewDispatcher.convert( viewTable );
+		const model = viewDispatcher.convert( viewTable, batch );
 		const modelFragment = new ModelDocumentFragment( model );
 
 		expect( modelToString( modelFragment ) )
@@ -680,7 +681,7 @@ describe( 'advanced-converters', () => {
 						}
 					}
 
-					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable ) );
+					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, batch ) );
 				}
 			}, { priority: 'lowest' } );
 
@@ -704,7 +705,7 @@ describe( 'advanced-converters', () => {
 			viewDispatcher.on( 'element:strong', ( evt, data, consumable, conversionApi ) => {
 				if ( consumable.consume( data.input, { name: true } ) ) {
 					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
+						data.output = conversionApi.convertChildren( data.input, consumable, batch );
 					}
 
 					for ( const child of data.output ) {
@@ -755,7 +756,7 @@ describe( 'advanced-converters', () => {
 				] )
 			] );
 
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, batch );
 
 			expect( modelToString( modelElement ) ).to.equal(
 				'<table cellpadding="5" cellspacing="5">' +

+ 41 - 33
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -6,6 +6,7 @@
 import buildViewConverter from '../../src/conversion/buildviewconverter';
 
 import ModelSchema from '../../src/model/schema';
+import ModelDocument from '../../src/model/document';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelTextProxy from '../../src/model/textproxy';
@@ -63,11 +64,15 @@ const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', '
 const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
 
 describe( 'View converter builder', () => {
-	let dispatcher, schema, objWithContext;
+	let dispatcher, schema, additionalData, batch;
+
+	const modelDocument = new ModelDocument();
 
 	beforeEach( () => {
+		batch = modelDocument.batch();
+
 		// `additionalData` parameter for `.convert` calls.
-		objWithContext = { context: [ '$root' ] };
+		additionalData = { context: [ '$root' ] };
 
 		schema = new ModelSchema();
 
@@ -95,7 +100,7 @@ describe( 'View converter builder', () => {
 	it( 'should convert from view element to model element', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), objWithContext );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -105,7 +110,7 @@ describe( 'View converter builder', () => {
 			.fromElement( 'img' )
 			.toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
 
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), objWithContext );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<image src="foo.jpg"></image>' );
 	} );
@@ -114,7 +119,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
 
 		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), objWithContext
+			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -127,7 +132,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( viewElement => ( { key: 'linkHref', value: viewElement.getAttribute( 'href' ) } ) );
 
 		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -142,7 +147,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -164,7 +169,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, objWithContext );
+		const conversionResult = dispatcher.convert( viewStructure, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal(
 			'<paragraph important="true">foo</paragraph>' +
@@ -190,7 +195,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
 	} );
@@ -207,7 +212,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -229,7 +234,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -255,7 +260,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		const marker1 = conversionResult.markers.get( 'marker1' );
 		const marker2 = conversionResult.markers.get( 'marker2' );
@@ -272,7 +277,7 @@ describe( 'View converter builder', () => {
 
 		const element = new ViewAttributeElement( 'span' );
 
-		const result = dispatcher.convert( element, objWithContext );
+		const result = dispatcher.convert( element, batch, additionalData );
 
 		expect( result ).to.be.instanceof( ModelDocumentFragment );
 		expect( result.childCount ).to.equal( 0 );
@@ -284,7 +289,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { class: 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, batch, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -296,7 +301,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, batch, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -308,7 +313,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, batch, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -325,7 +330,7 @@ describe( 'View converter builder', () => {
 
 		// Not quite megatron.
 		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -333,7 +338,8 @@ describe( 'View converter builder', () => {
 		// Almost a megatron. Missing a head.
 		result = dispatcher.convert(
 			new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ),
-			objWithContext
+			batch,
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -345,7 +351,8 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			objWithContext
+			batch,
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
@@ -357,7 +364,8 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			objWithContext
+			batch,
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
@@ -377,7 +385,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'foo' )
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
 	} );
@@ -387,7 +395,7 @@ describe( 'View converter builder', () => {
 
 		const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
 	} );
@@ -399,7 +407,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		// Element converter was fired first even though attribute converter was added first.
@@ -415,7 +423,7 @@ describe( 'View converter builder', () => {
 		let result;
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -425,7 +433,7 @@ describe( 'View converter builder', () => {
 			.toElement( 'customP' );
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
@@ -446,7 +454,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( 'size', 'small' );
 
 		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		// P element and it's children got converted by the converter (1) and the converter (1) got fired
 		// because P name was not consumed in converter (2). Converter (3) could consume class="small" because
@@ -469,7 +477,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, objWithContext );
+		const conversionResult = dispatcher.convert( viewStructure, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
 	} );
@@ -488,7 +496,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -507,7 +515,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -521,11 +529,11 @@ describe( 'View converter builder', () => {
 			} );
 
 		const viewElement = new ViewContainerElement( 'p' );
-		let conversionResult = dispatcher.convert( viewElement, objWithContext );
+		let conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 
 		viewElement.setAttribute( 'stop', true );
-		conversionResult = dispatcher.convert( viewElement, objWithContext );
+		conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 
 		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 		expect( conversionResult.childCount ).to.equal( 0 );
@@ -543,11 +551,11 @@ describe( 'View converter builder', () => {
 		} );
 
 		const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } );
-		let conversionResult = dispatcher.convert( viewElement, objWithContext );
+		let conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph type="foo"></paragraph>' );
 
 		viewElement.setAttribute( 'data-type', 'stop' );
-		conversionResult = dispatcher.convert( viewElement, objWithContext );
+		conversionResult = dispatcher.convert( viewElement, batch, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 	} );
 } );

+ 26 - 18
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -63,8 +63,7 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.on( 'insert:image', cbInsertImage );
 			dispatcher.on( 'addAttribute:key:$text', cbAddAttribute );
 
-			const insertedText = new ModelText( 'foo', { key: 'value' } );
-			doc.batch().insert( ModelPosition.createFromParentAndOffset( root, 0 ), insertedText );
+			doc.batch().insertText( 'foo', { key: 'value' }, root );
 
 			expect( cbInsertText.called ).to.be.true;
 			expect( cbAddAttribute.called ).to.be.true;
@@ -113,13 +112,13 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.on( 'addAttribute:key:$text', cbAddText );
 			dispatcher.on( 'addAttribute:key:image', cbAddImage );
 
-			doc.batch().setAttribute( image, 'key', 'value' );
+			doc.batch().setAttribute( 'key', 'value', image );
 
 			// Callback for adding attribute on text not called.
 			expect( cbAddText.called ).to.be.false;
 			expect( cbAddImage.calledOnce ).to.be.true;
 
-			doc.batch().setAttribute( ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ), 'key', 'value' );
+			doc.batch().setAttribute( 'key', 'value', ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ) );
 
 			expect( cbAddText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -129,18 +128,21 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire changeAttribute callbacks for change attribute change', () => {
 			const cbChangeText = sinon.spy();
 			const cbChangeImage = sinon.spy();
+			const batch = doc.batch();
 
 			dispatcher.on( 'changeAttribute:key:$text', cbChangeText );
 			dispatcher.on( 'changeAttribute:key:image', cbChangeImage );
 
-			doc.batch().setAttribute( image, 'key', 'value' ).setAttribute( image, 'key', 'newValue' );
+			batch.setAttribute( 'key', 'value', image );
+			batch.setAttribute( 'key', 'newValue', image );
 
 			// Callback for adding attribute on text not called.
 			expect( cbChangeText.called ).to.be.false;
 			expect( cbChangeImage.calledOnce ).to.be.true;
 
 			const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 );
-			doc.batch().setAttribute( range, 'key', 'value' ).setAttribute( range, 'key', 'newValue' );
+			batch.setAttribute( 'key', 'value', range );
+			batch.setAttribute( 'key', 'newValue', range );
 
 			expect( cbChangeText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -150,18 +152,21 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire removeAttribute callbacks for remove attribute change', () => {
 			const cbRemoveText = sinon.spy();
 			const cbRemoveImage = sinon.spy();
+			const batch = doc.batch();
 
 			dispatcher.on( 'removeAttribute:key:$text', cbRemoveText );
 			dispatcher.on( 'removeAttribute:key:image', cbRemoveImage );
 
-			doc.batch().setAttribute( image, 'key', 'value' ).removeAttribute( image, 'key' );
+			batch.setAttribute( 'key', 'value', image );
+			batch.removeAttribute( 'key', image );
 
 			// Callback for adding attribute on text not called.
 			expect( cbRemoveText.called ).to.be.false;
 			expect( cbRemoveImage.calledOnce ).to.be.true;
 
 			const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 );
-			doc.batch().setAttribute( range, 'key', 'value' ).removeAttribute( range, 'key' );
+			batch.setAttribute( 'key', 'value', range );
+			batch.removeAttribute( 'key', range );
 
 			expect( cbRemoveText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -182,7 +187,7 @@ describe( 'ModelConversionDispatcher', () => {
 			const gyNode = new ModelElement( 'image' );
 			doc.graveyard.appendChildren( gyNode );
 
-			doc.batch().setAttribute( gyNode, 'key', 'value' );
+			doc.batch().setAttribute( 'key', 'value', gyNode );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -616,9 +621,10 @@ describe( 'ModelConversionDispatcher', () => {
 
 		it( 'should prepare correct list of consumable values', () => {
 			doc.enqueueChanges( () => {
-				doc.batch()
-					.setAttribute( ModelRange.createIn( root ), 'bold', true )
-					.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true );
+				const batch = doc.batch();
+
+				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.on( 'selection', ( evt, data, consumable ) => {
@@ -634,9 +640,10 @@ describe( 'ModelConversionDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			doc.enqueueChanges( () => {
-				doc.batch()
-					.setAttribute( ModelRange.createIn( root ), 'bold', true )
-					.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true );
+				const batch = doc.batch();
+
+				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.convertSelection( doc.selection, [] );
@@ -653,9 +660,10 @@ describe( 'ModelConversionDispatcher', () => {
 			} );
 
 			doc.enqueueChanges( () => {
-				doc.batch()
-					.setAttribute( ModelRange.createIn( root ), 'bold', true )
-					.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true );
+				const batch = doc.batch();
+
+				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.convertSelection( doc.selection, [] );

+ 13 - 9
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -9,6 +9,7 @@ import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
 import ModelSchema from '../../src/model/schema';
+import ModelDocument from '../../src/model/document';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -16,13 +17,16 @@ import ModelText from '../../src/model/text';
 import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
 
 describe( 'view-to-model-converters', () => {
-	let dispatcher, schema, objWithContext;
+	let dispatcher, schema, additionalData, batch;
+
+	const modelDocument = new ModelDocument();
 
 	beforeEach( () => {
 		schema = new ModelSchema();
 		schema.registerItem( 'paragraph', '$block' );
 		schema.allow( { name: '$text', inside: '$root' } );
-		objWithContext = { context: [ '$root' ] };
+		batch = modelDocument.batch();
+		additionalData = { context: [ '$root' ] };
 		dispatcher = new ViewConversionDispatcher( { schema } );
 	} );
 
@@ -32,7 +36,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -51,7 +55,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -64,12 +68,12 @@ describe( 'view-to-model-converters', () => {
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
 
-			let conversionResult = dispatcher.convert( viewText, objWithContext );
+			let conversionResult = dispatcher.convert( viewText, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 
-			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } );
+			conversionResult = dispatcher.convert( viewText, batch, { context: [ '$block' ] } );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -82,7 +86,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -103,7 +107,7 @@ describe( 'view-to-model-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 
-			const conversionResult = dispatcher.convert( viewFragment, objWithContext );
+			const conversionResult = dispatcher.convert( viewFragment, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -128,7 +132,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewP, objWithContext );
+			const conversionResult = dispatcher.convert( viewP, batch, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );

+ 24 - 15
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -11,6 +11,7 @@ import ViewText from '../../src/view/text';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
 import ModelDocumentFragment from '../../src/model/documentfragment';
+import ModelDocument from '../../src/model/document';
 import { stringify } from '../../src/dev-utils/model';
 
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -35,10 +36,13 @@ describe( 'ViewConversionDispatcher', () => {
 	} );
 
 	describe( 'convert', () => {
-		let dispatcher;
+		let dispatcher, batch;
+
+		const modelDocument = new ModelDocument();
 
 		beforeEach( () => {
 			dispatcher = new ViewConversionDispatcher();
+			batch = modelDocument.batch();
 		} );
 
 		it( 'should fire viewCleanup event on converted view part', () => {
@@ -47,7 +51,7 @@ describe( 'ViewConversionDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
-			dispatcher.convert( viewP );
+			dispatcher.convert( viewP, batch );
 
 			expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true;
 		} );
@@ -61,9 +65,9 @@ describe( 'ViewConversionDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convert( viewText );
-			dispatcher.convert( viewElement );
-			dispatcher.convert( viewFragment );
+			dispatcher.convert( viewText, batch );
+			dispatcher.convert( viewElement, batch );
+			dispatcher.convert( viewFragment, batch );
 
 			expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true;
@@ -95,7 +99,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewText, batch, { foo: 'bar' } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -130,7 +134,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewElement, batch, { foo: 'bar' } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -164,7 +168,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewFragment, batch, { foo: 'bar' } );
 
 			// Check conversion result.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
@@ -187,7 +191,7 @@ describe( 'ViewConversionDispatcher', () => {
 				] );
 			} );
 
-			const conversionResult = dispatcher.convert( viewFragment );
+			const conversionResult = dispatcher.convert( viewFragment, batch );
 
 			expect( conversionResult.markers.size ).to.equal( 2 );
 			expect( stringify( conversionResult, conversionResult.markers.get( 'marker1' ) ) ).to.deep.equal( 'fo[ob]ar' );
@@ -197,9 +201,13 @@ describe( 'ViewConversionDispatcher', () => {
 
 	describe( 'conversionApi', () => {
 		let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher,
-			spyNull, spyArray, viewDiv, viewNull, viewArray;
+			spyNull, spyArray, viewDiv, viewNull, viewArray, batch;
+
+		const modelDocument = new ModelDocument();
 
 		beforeEach( () => {
+			batch = modelDocument.batch();
+
 			spy = sinon.spy();
 			spyP = sinon.spy();
 			spyText = sinon.spy();
@@ -260,9 +268,10 @@ describe( 'ViewConversionDispatcher', () => {
 
 					expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP );
 					expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText );
+					expect( conversionApi.batch ).to.equal( batch );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment(), batch, { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -279,7 +288,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				dispatcher.convert( new ViewDocumentFragment(), batch );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;
@@ -297,7 +306,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				dispatcher.convert( new ViewDocumentFragment(), batch );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyArray.calledOnce ).to.be.true;
@@ -323,7 +332,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), batch, { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -344,7 +353,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), batch, { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;

+ 36 - 1
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -13,6 +13,7 @@ import ModelText from '../../src/model/text';
 import ModelTextProxy from '../../src/model/textproxy';
 import ModelElement from '../../src/model/element';
 import AttributeOperation from '../../src/model/operation/attributeoperation';
+import DetachOperation from '../../src/model/operation/detachoperation';
 import InsertOperation from '../../src/model/operation/insertoperation';
 import MarkerOperation from '../../src/model/operation/markeroperation';
 import MoveOperation from '../../src/model/operation/moveoperation';
@@ -22,12 +23,13 @@ import RootAttributeOperation from '../../src/model/operation/rootattributeopera
 import RemoveOperation from '../../src/model/operation/removeoperation';
 import DeltaFactory from '../../src/model/delta/deltafactory';
 import Delta from '../../src/model/delta/delta';
-import { default as AttributeDelta, RootAttributeDelta } from '../../src/model/delta/attributedelta';
+import AttributeDelta from '../../src/model/delta/attributedelta';
 import InsertDelta from '../../src/model/delta/insertdelta';
 import MarkerDelta from '../../src/model/delta/markerdelta';
 import MergeDelta from '../../src/model/delta/mergedelta';
 import MoveDelta from '../../src/model/delta/movedelta';
 import RenameDelta from '../../src/model/delta/renamedelta';
+import RootAttributeDelta from '../../src/model/delta/rootattributedelta';
 import SplitDelta from '../../src/model/delta/splitdelta';
 import UnwrapDelta from '../../src/model/delta/unwrapdelta';
 import WrapDelta from '../../src/model/delta/wrapdelta';
@@ -213,6 +215,39 @@ describe( 'debug tools', () => {
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
 			} );
 
+			it( 'DetachOperation (text node)', () => {
+				const op = new DetachOperation( ModelPosition.createAt( modelRoot, 0 ), 3, 0 );
+
+				expect( op.toString() ).to.equal( 'DetachOperation( 0 ): #foo -> main [ 0 ] - [ 3 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
+			} );
+
+			it( 'DetachOperation (element)', () => {
+				const element = new ModelElement( 'element' );
+				modelRoot.insertChildren( 0, element );
+
+				const op = new DetachOperation( ModelPosition.createBefore( element ), 1, 0 );
+
+				expect( op.toString() ).to.equal( 'DetachOperation( 0 ): <element> -> main [ 0 ] - [ 1 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
+			} );
+
+			it( 'DetachOperation (multiple nodes)', () => {
+				const element = new ModelElement( 'element' );
+				modelRoot.insertChildren( 0, element );
+
+				const op = new DetachOperation( ModelPosition.createBefore( element ), 2, 0 );
+
+				expect( op.toString() ).to.equal( 'DetachOperation( 0 ): [ 2 ] -> main [ 0 ] - [ 2 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
+			} );
+
 			it( 'InsertOperation (text node)', () => {
 				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
 

+ 12 - 12
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -497,21 +497,21 @@ describe( 'model test utils', () => {
 
 		it( 'throws when invalid XML', () => {
 			expect( () => {
-				parse( '<a><b></a></b>', document.schema );
+				parse( '<a><b></a></b>', document.schema, document.batch() );
 			} ).to.throw( Error, /Parse error/ );
 		} );
 
 		it( 'throws when try to set element not registered in schema', () => {
 			expect( () => {
-				parse( '<xyz></xyz>', document.schema );
+				parse( '<xyz></xyz>', document.schema, document.batch() );
 			} ).to.throw( Error, 'Element \'xyz\' not allowed in context ["$root"].' );
 		} );
 
 		it( 'throws when try to set text directly to $root without registering it', () => {
-			const doc = new Document();
+			const document = new Document();
 
 			expect( () => {
-				parse( 'text', doc.schema );
+				parse( 'text', document.schema, document.batch() );
 			} ).to.throw( Error, 'Element \'$text\' not allowed in context ["$root"].' );
 		} );
 
@@ -521,7 +521,7 @@ describe( 'model test utils', () => {
 			doc.schema.allow( { name: '$text', inside: 'foo' } );
 
 			expect( () => {
-				parse( 'text', doc.schema, { context: [ 'foo' ] } );
+				parse( 'text', doc.schema, doc.batch(), { context: [ 'foo' ] } );
 			} ).to.not.throw();
 		} );
 
@@ -556,7 +556,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection attributes', () => {
-				const result = parse( 'foo[]bar', document.schema, { selectionAttributes: {
+				const result = parse( 'foo[]bar', document.schema, document.batch(), { selectionAttributes: {
 					bold: true,
 					italic: true
 				} } );
@@ -577,7 +577,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection with attribute containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, { selectionAttributes: {
+				const result = parse( 'x[<a></a>]', document.schema, document.batch(), { selectionAttributes: {
 					bold: true
 				} } );
 
@@ -586,7 +586,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets a backward selection containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, {
+				const result = parse( 'x[<a></a>]', document.schema, document.batch(), {
 					lastRangeBackward: true
 				} );
 
@@ -599,7 +599,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection within a text with different attributes', () => {
-				const result = parse( '<$text bold="true">fo[o</$text>ba]r', document.schema, {
+				const result = parse( '<$text bold="true">fo[o</$text>ba]r', document.schema, document.batch(), {
 					selectionAttributes: { bold: true }
 				} );
 
@@ -609,13 +609,13 @@ describe( 'model test utils', () => {
 
 			it( 'throws when missing selection start', () => {
 				expect( () => {
-					parse( 'foo]' );
+					parse( 'foo]', document.schema, document.batch() );
 				} ).to.throw( Error );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					parse( '[foo' );
+					parse( '[foo', document.schema, document.batch() );
 				} ).to.throw( Error );
 			} );
 		} );
@@ -623,7 +623,7 @@ describe( 'model test utils', () => {
 		function test( title, options ) {
 			it( title, () => {
 				const output = options.output || options.data;
-				const data = parse( options.data, document.schema );
+				const data = parse( options.data, document.schema, document.batch() );
 				let model, selection;
 
 				if ( data.selection && data.model ) {

+ 1 - 1
packages/ckeditor5-engine/tests/manual/tickets/475/1.js

@@ -80,7 +80,7 @@ class AutoLinker extends Plugin {
 
 				doc.enqueueChanges( () => {
 					const urlRange = Range.createFromPositionAndShift( livePos, url.length );
-					batch.setAttribute( urlRange, 'link', url );
+					batch.setAttribute( 'link', url, urlRange );
 				} );
 			}
 		} );

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1868 - 42
packages/ckeditor5-engine/tests/model/batch.js


+ 1 - 405
packages/ckeditor5-engine/tests/model/delta/attributedelta.js

@@ -3,412 +3,14 @@
  * For licensing, see LICENSE.md.
  */
 
-import count from '@ckeditor/ckeditor5-utils/src/count';
 import Document from '../../../src/model/document';
-import Text from '../../../src/model/text';
 import Range from '../../../src/model/range';
 import Position from '../../../src/model/position';
-import Element from '../../../src/model/element';
-import { default as AttributeDelta, RootAttributeDelta } from '../../../src/model/delta/attributedelta';
+import AttributeDelta from '../../../src/model/delta/attributedelta';
 import AttributeOperation from '../../../src/model/operation/attributeoperation';
 import NoOperation from '../../../src/model/operation/nooperation';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
-describe( 'Batch', () => {
-	let batch, doc, root;
-
-	const correctDeltaMatcher = sinon.match( operation => {
-		return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-	} );
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-		batch = doc.batch();
-	} );
-
-	function getOperationsCount() {
-		let totalNumber = 0;
-
-		for ( const delta of batch.deltas ) {
-			totalNumber += count( delta.operations );
-		}
-
-		return totalNumber;
-	}
-
-	describe( 'change attribute on node', () => {
-		let node, text;
-
-		beforeEach( () => {
-			node = new Element( 'p', { a: 1 } );
-			text = new Text( 'c', { a: 1 } );
-
-			root.insertChildren( 0, [ node, text ] );
-		} );
-
-		describe( 'setAttribute', () => {
-			it( 'should create the attribute on element', () => {
-				batch.setAttribute( node, 'b', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.getAttribute( 'b' ) ).to.equal( 2 );
-			} );
-
-			it( 'should change the attribute of element', () => {
-				batch.setAttribute( node, 'a', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.getAttribute( 'a' ) ).to.equal( 2 );
-			} );
-
-			it( 'should create the attribute on text node', () => {
-				batch.setAttribute( text, 'b', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 );
-			} );
-
-			it( 'should change the attribute of text node', () => {
-				batch.setAttribute( text, 'a', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 );
-			} );
-
-			it( 'should do nothing if the attribute value is the same', () => {
-				batch.setAttribute( node, 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( node.getAttribute( 'a' ) ).to.equal( 1 );
-			} );
-
-			it( 'should be chainable', () => {
-				const chain = batch.setAttribute( node, 'b', 2 );
-				expect( chain ).to.equal( batch );
-			} );
-
-			it( 'should add delta to batch and operation to delta before applying operation', () => {
-				sinon.spy( doc, 'applyOperation' );
-				batch.setAttribute( node, 'b', 2 );
-
-				expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-			} );
-		} );
-
-		describe( 'removeAttribute', () => {
-			it( 'should remove the attribute from element', () => {
-				batch.removeAttribute( node, 'a' );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( node.getAttribute( 'a' ) ).to.be.undefined;
-			} );
-
-			it( 'should remove the attribute from character', () => {
-				batch.removeAttribute( text, 'a' );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined;
-			} );
-
-			it( 'should do nothing if the attribute is not set', () => {
-				batch.removeAttribute( node, 'b' );
-				expect( getOperationsCount() ).to.equal( 0 );
-			} );
-
-			it( 'should be chainable', () => {
-				const chain = batch.removeAttribute( node, 'a' );
-				expect( chain ).to.equal( batch );
-			} );
-
-			it( 'should add delta to batch and operation to delta before applying operation', () => {
-				sinon.spy( doc, 'applyOperation' );
-				batch.removeAttribute( node, 'a' );
-
-				expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-			} );
-		} );
-	} );
-
-	describe( 'change attribute on range', () => {
-		beforeEach( () => {
-			root.insertChildren( 0, [
-				new Text( 'xxx', { a: 1 } ),
-				new Text( 'xxx' ),
-				new Text( 'xxx', { a: 1 } ),
-				new Text( 'xxx', { a: 2 } ),
-				new Text( 'xxx' ),
-				new Text( 'xxx', { a: 1 } ),
-				new Element( 'e', { a: 2 }, new Text( 'xxx' ) ),
-				new Text( 'xxx' )
-			] );
-		} );
-
-		function getRange( startIndex, endIndex ) {
-			return new Range(
-				Position.createFromParentAndOffset( root, startIndex ),
-				Position.createFromParentAndOffset( root, endIndex )
-			);
-		}
-
-		function getChangesAttrsCount() {
-			let totalNumber = 0;
-
-			for ( const delta of batch.deltas ) {
-				for ( const operation of delta.operations ) {
-					totalNumber += count( operation.range.getItems( { singleCharacters: true } ) );
-				}
-			}
-
-			return totalNumber;
-		}
-
-		function getCompressedAttrs() {
-			// default: 111---111222---1112------
-			const range = Range.createIn( root );
-
-			return Array.from( range.getItems( { singleCharacters: true } ) )
-				.map( item => item.getAttribute( 'a' ) || '-' )
-				.join( '' );
-		}
-
-		describe( 'setAttribute', () => {
-			it( 'should set the attribute on the range', () => {
-				batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 3 );
-				expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' );
-			} );
-
-			it( 'should split the operations if parts of the range have different attributes', () => {
-				batch.setAttribute( getRange( 4, 14 ), 'a', 3 );
-				expect( getOperationsCount() ).to.equal( 4 );
-				expect( getChangesAttrsCount() ).to.equal( 10 );
-				expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' );
-			} );
-
-			it( 'should split the operations if parts of the part of the range have the attribute', () => {
-				batch.setAttribute( getRange( 4, 14 ), 'a', 2 );
-				expect( getOperationsCount() ).to.equal( 3 );
-				expect( getChangesAttrsCount() ).to.equal( 7 );
-				expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' );
-			} );
-
-			it( 'should strip the range if the beginning have the attribute', () => {
-				batch.setAttribute( getRange( 1, 5 ), 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 2 );
-				expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' );
-			} );
-
-			it( 'should strip the range if the ending have the attribute', () => {
-				batch.setAttribute( getRange( 13, 17 ), 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 2 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' );
-			} );
-
-			it( 'should do nothing if the range has attribute', () => {
-				batch.setAttribute( getRange( 0, 3 ), 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should not check range\'s start position node when creating operations', () => {
-				const range = new Range(
-					new Position( root, [ 18, 1 ] ),
-					new Position( root, [ 19 ] )
-				);
-
-				batch.setAttribute( range, 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 2 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' );
-			} );
-
-			it( 'should not change elements attribute if range contains closing tag', () => {
-				const range = new Range(
-					new Position( root, [ 18, 1 ] ),
-					new Position( root, [ 21 ] )
-				);
-
-				batch.setAttribute( range, 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 4 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' );
-			} );
-
-			it( 'should not create an operation if the range contains only closing tag', () => {
-				const range = new Range(
-					new Position( root, [ 18, 3 ] ),
-					new Position( root, [ 19 ] )
-				);
-
-				batch.setAttribute( range, 'a', 3 );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should not create an operation if is collapsed', () => {
-				batch.setAttribute( getRange( 3, 3 ), 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should create a proper operations for the mixed range', () => {
-				batch.setAttribute( getRange( 0, 20 ), 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 5 );
-				expect( getChangesAttrsCount() ).to.equal( 14 );
-				expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' );
-			} );
-
-			it( 'should be chainable', () => {
-				const chain = batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
-				expect( chain ).to.equal( batch );
-			} );
-
-			it( 'should add delta to batch and operation to delta before applying operation', () => {
-				sinon.spy( doc, 'applyOperation' );
-				batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
-
-				expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-			} );
-		} );
-
-		describe( 'removeAttribute', () => {
-			it( 'should remove the attribute on the range', () => {
-				batch.removeAttribute( getRange( 0, 2 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 2 );
-				expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' );
-			} );
-
-			it( 'should split the operations if parts of the range have different attributes', () => {
-				batch.removeAttribute( getRange( 7, 11 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( getChangesAttrsCount() ).to.equal( 4 );
-				expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' );
-			} );
-
-			it( 'should split the operations if parts of the part of the range have no attribute', () => {
-				batch.removeAttribute( getRange( 1, 7 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( getChangesAttrsCount() ).to.equal( 3 );
-				expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' );
-			} );
-
-			it( 'should strip the range if the beginning have no attribute', () => {
-				batch.removeAttribute( getRange( 4, 12 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( getChangesAttrsCount() ).to.equal( 6 );
-				expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
-			} );
-
-			it( 'should strip the range if the ending have no attribute', () => {
-				batch.removeAttribute( getRange( 7, 15 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( getChangesAttrsCount() ).to.equal( 5 );
-				expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' );
-			} );
-
-			it( 'should do nothing if the range has no attribute', () => {
-				batch.removeAttribute( getRange( 4, 5 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should not check range\'s start position node when creating operations', () => {
-				const range = new Range(
-					new Position( root, [ 18, 3 ] ),
-					new Position( root, [ 19 ] )
-				);
-
-				batch.removeAttribute( range, 'a' );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getChangesAttrsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should not apply operation twice in the range contains opening and closing tags', () => {
-				batch.removeAttribute( getRange( 18, 22 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( getChangesAttrsCount() ).to.equal( 1 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' );
-			} );
-
-			it( 'should not create an operation if range is collapsed', () => {
-				batch.removeAttribute( getRange( 3, 3 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 0 );
-				expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-			} );
-
-			it( 'should create a proper operations for the mixed range', () => {
-				batch.removeAttribute( getRange( 3, 15 ), 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( getChangesAttrsCount() ).to.equal( 6 );
-				expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
-			} );
-
-			it( 'should be chainable', () => {
-				const chain = batch.removeAttribute( getRange( 0, 2 ), 'a' );
-				expect( chain ).to.equal( batch );
-			} );
-
-			it( 'should add delta to batch and operation to delta before applying operation', () => {
-				sinon.spy( doc, 'applyOperation' );
-				batch.removeAttribute( getRange( 0, 2 ), 'a' );
-
-				expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-			} );
-		} );
-	} );
-
-	describe( 'change attribute on root element', () => {
-		describe( 'setAttribute', () => {
-			it( 'should create the attribute on root', () => {
-				batch.setAttribute( root, 'b', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getAttribute( 'b' ) ).to.equal( 2 );
-			} );
-
-			it( 'should change the attribute of root', () => {
-				batch.setAttribute( root, 'a', 2 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getAttribute( 'a' ) ).to.equal( 2 );
-			} );
-
-			it( 'should do nothing if the attribute value is the same', () => {
-				batch.setAttribute( root, 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				batch.setAttribute( root, 'a', 1 );
-				expect( getOperationsCount() ).to.equal( 1 );
-				expect( root.getAttribute( 'a' ) ).to.equal( 1 );
-			} );
-		} );
-
-		describe( 'removeAttribute', () => {
-			it( 'should remove the attribute from root', () => {
-				batch.setAttribute( root, 'a', 1 );
-				batch.removeAttribute( root, 'a' );
-				expect( getOperationsCount() ).to.equal( 2 );
-				expect( root.getAttribute( 'a' ) ).to.be.undefined;
-			} );
-
-			it( 'should do nothing if the attribute is not set', () => {
-				batch.removeAttribute( root, 'b' );
-				expect( getOperationsCount() ).to.equal( 0 );
-			} );
-		} );
-	} );
-
-	it( 'should not add empty delta to the batch', () => {
-		const nodeA = new Element( 'p', { a: 1 } );
-		const nodeB = new Element( 'p', { b: 2 } );
-		root.insertChildren( 0, [ nodeA, nodeB ] );
-
-		batch.setAttribute( nodeA, 'a', 1 );
-
-		expect( batch.deltas.length ).to.equal( 0 );
-
-		batch.removeAttribute( Range.createIn( root ), 'x' );
-
-		expect( batch.deltas.length ).to.equal( 0 );
-	} );
-} );
-
 describe( 'AttributeDelta', () => {
 	let doc, root, delta;
 
@@ -526,9 +128,3 @@ describe( 'AttributeDelta', () => {
 		expect( json ).not.to.have.property( '_range' );
 	} );
 } );
-
-describe( 'RootAttributeDelta', () => {
-	it( 'should provide proper className', () => {
-		expect( RootAttributeDelta.className ).to.equal( 'engine.model.delta.RootAttributeDelta' );
-	} );
-} );

+ 0 - 88
packages/ckeditor5-engine/tests/model/delta/insertdelta.js

@@ -5,102 +5,14 @@
 
 import Document from '../../../src/model/document';
 import Element from '../../../src/model/element';
-import DocumentFragment from '../../../src/model/documentfragment';
-import Text from '../../../src/model/text';
 import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
 
 import InsertOperation from '../../../src/model/operation/insertoperation';
-import MarkerOperation from '../../../src/model/operation/markeroperation';
 import InsertDelta from '../../../src/model/delta/insertdelta';
 
 import RemoveDelta from '../../../src/model/delta/removedelta';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 
-import { stringify } from '../../../src/dev-utils/model';
-
-describe( 'Batch', () => {
-	let doc, root, batch, p, ul, chain;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-		root.insertChildren( 0, new Text( 'abc' ) );
-
-		batch = doc.batch();
-
-		p = new Element( 'p' );
-		ul = new Element( 'ul' );
-
-		chain = batch.insert( new Position( root, [ 2 ] ), [ p, ul ] );
-	} );
-
-	describe( 'insert', () => {
-		it( 'should insert given nodes at given position', () => {
-			expect( root.childCount ).to.equal( 4 );
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 1 ) ).to.equal( p );
-			expect( root.getChild( 2 ) ).to.equal( ul );
-		} );
-
-		it( 'should be chainable', () => {
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch.insert( new Position( root, [ 2 ] ), [ p, ul ] );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-
-		it( 'should transfer markers from given DocumentFragment', () => {
-			const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] );
-			const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) );
-
-			documentFragment.markers.set( 'marker', marker );
-
-			batch.insert( new Position( root, [ 3, 0 ] ), documentFragment );
-
-			expect( Array.from( doc.markers ).length ).to.equal( 1 );
-			expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab<p></p><ul><li>f[oo b]ar</li></ul>c' );
-		} );
-
-		it( 'should set each marker as separate operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-
-			const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] );
-			const marker1 = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 2 ] ) );
-			const marker2 = new Range( new Position( documentFragment, [ 0, 5 ] ), new Position( documentFragment, [ 0, 6 ] ) );
-
-			documentFragment.markers.set( 'marker1', marker1 );
-			documentFragment.markers.set( 'marker2', marker2 );
-
-			batch.insert( new Position( root, [ 3, 0 ] ), documentFragment );
-
-			expect( doc.applyOperation.calledThrice );
-			expect( doc.applyOperation.firstCall.calledWith( sinon.match( operation => operation instanceof InsertOperation ) ) );
-			expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) );
-			expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) );
-		} );
-
-		it( 'should not create a delta and an operation if no nodes were inserted', () => {
-			sinon.spy( doc, 'applyOperation' );
-
-			batch = doc.batch();
-
-			batch.insert( new Position( root, [ 0 ] ), [] );
-
-			expect( batch.deltas.length ).to.equal( 0 );
-			expect( doc.applyOperation.called ).to.be.false;
-		} );
-	} );
-} );
-
 describe( 'InsertDelta', () => {
 	let insertDelta, doc, root;
 

+ 0 - 111
packages/ckeditor5-engine/tests/model/delta/markerdelta.js

@@ -5,121 +5,10 @@
 
 import Document from '../../../src/model/document';
 import Range from '../../../src/model/range';
-import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import MarkerDelta from '../../../src/model/delta/markerdelta';
 import MarkerOperation from '../../../src/model/operation/markeroperation';
 
-describe( 'Batch', () => {
-	let doc, root, range;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-		root.appendChildren( new Text( 'foo' ) );
-		range = Range.createIn( root );
-	} );
-
-	describe( 'setMarker', () => {
-		it( 'should add marker to the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
-		} );
-
-		it( 'should update marker in the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-
-			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
-			doc.batch().setMarker( 'name', range2 );
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
-		} );
-
-		it( 'should accept marker instance', () => {
-			doc.batch().setMarker( 'name', range );
-			const marker = doc.markers.get( 'name' );
-			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
-
-			const batch = doc.batch().setMarker( marker, range2 );
-			const op = batch.deltas[ 0 ].operations[ 0 ];
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
-			expect( op.oldRange.isEqual( range ) ).to.be.true;
-			expect( op.newRange.isEqual( range2 ) ).to.be.true;
-		} );
-
-		it( 'should accept empty range parameter if marker instance is passed', () => {
-			const marker = doc.markers.set( 'name', range );
-
-			sinon.spy( doc, 'fire' );
-
-			doc.on( 'change', ( evt, type, changes ) => {
-				if ( type == 'marker' ) {
-					expect( changes.type ).to.equal( 'set' );
-					expect( changes.name ).to.equal( 'name' );
-				}
-			} );
-
-			const batch = doc.batch().setMarker( marker );
-			const op = batch.deltas[ 0 ].operations[ 0 ];
-
-			expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true;
-			expect( op.oldRange ).to.be.null;
-			expect( op.newRange.isEqual( range ) ).to.be.true;
-		} );
-
-		it( 'should throw if marker with given name does not exist and range is not passed', () => {
-			expect( () => {
-				doc.batch().setMarker( 'name' );
-			} ).to.throw( CKEditorError, /^batch-setMarker-no-range/ );
-		} );
-	} );
-
-	describe( 'removeMarker', () => {
-		it( 'should remove marker from the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-			doc.batch().removeMarker( 'name' );
-
-			expect( doc.markers.get( 'name' ) ).to.be.null;
-		} );
-
-		it( 'should throw when trying to remove non existing marker', () => {
-			expect( () => {
-				doc.batch().removeMarker( 'name' );
-			} ).to.throw( CKEditorError, /^batch-removeMarker-no-marker/ );
-		} );
-
-		it( 'should accept marker instance', () => {
-			doc.batch().setMarker( 'name', range );
-			const marker = doc.markers.get( 'name' );
-
-			doc.batch().removeMarker( marker );
-
-			expect( doc.markers.get( 'name' ) ).to.be.null;
-		} );
-	} );
-
-	it( 'should be chainable', () => {
-		const batch = doc.batch();
-		const chain = batch.setMarker( 'name', range );
-
-		expect( chain ).to.equal( batch );
-	} );
-
-	it( 'should add delta to batch and operation to delta before applying operation', () => {
-		sinon.spy( doc, 'applyOperation' );
-		const batch = doc.batch().setMarker( 'name', range );
-
-		const correctDeltaMatcher = sinon.match( operation => {
-			return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-		} );
-
-		expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-	} );
-} );
-
 describe( 'MarkerDelta', () => {
 	let markerDelta, doc, root, range;
 

+ 0 - 62
packages/ckeditor5-engine/tests/model/delta/mergedelta.js

@@ -5,9 +5,6 @@
 
 import Document from '../../../src/model/document';
 import Position from '../../../src/model/position';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import MergeDelta from '../../../src/model/delta/mergedelta';
 import SplitDelta from '../../../src/model/delta/splitdelta';
@@ -16,65 +13,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 
-import count from '@ckeditor/ckeditor5-utils/src/count';
-
-describe( 'Batch', () => {
-	let doc, root, p1, p2, batch;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) );
-		p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) );
-
-		root.insertChildren( 0, [ p1, p2 ] );
-	} );
-
-	describe( 'merge', () => {
-		it( 'should merge foo and bar into foobar', () => {
-			doc.batch().merge( new Position( root, [ 1 ] ) );
-
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
-		} );
-
-		it( 'should throw if there is no element after', () => {
-			expect( () => {
-				doc.batch().merge( new Position( root, [ 2 ] ) );
-			} ).to.throw( CKEditorError, /^batch-merge-no-element-after/ );
-		} );
-
-		it( 'should throw if there is no element before', () => {
-			expect( () => {
-				doc.batch().merge( new Position( root, [ 0, 2 ] ) );
-			} ).to.throw( CKEditorError, /^batch-merge-no-element-before/ );
-		} );
-
-		it( 'should be chainable', () => {
-			batch = doc.batch();
-
-			const chain = batch.merge( new Position( root, [ 1 ] ) );
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch = doc.batch().merge( new Position( root, [ 1 ] ) );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'MergeDelta', () => {
 	let mergeDelta, doc, root;
 

+ 0 - 67
packages/ckeditor5-engine/tests/model/delta/movedelta.js

@@ -3,79 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
-import { getNodesAndText } from '../../../tests/model/_utils/utils';
 import Document from '../../../src/model/document';
 import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import MoveDelta from '../../../src/model/delta/movedelta';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 
-describe( 'Batch', () => {
-	let doc, root, div, p, batch, chain;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		div = new Element( 'div', [], new Text( 'foobar' ) );
-		p = new Element( 'p', [], new Text( 'abcxyz' ) );
-
-		div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
-		div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );
-
-		root.insertChildren( 0, [ div, p ] );
-
-		batch = doc.batch();
-	} );
-
-	describe( 'move', () => {
-		it( 'should move specified node', () => {
-			batch.move( div, new Position( root, [ 2 ] ) );
-
-			expect( root.maxOffset ).to.equal( 2 );
-			expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
-			expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'PggggPfoobarPhhhhP' );
-		} );
-
-		it( 'should move flat range of nodes', () => {
-			const range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) );
-			batch.move( range, new Position( root, [ 1, 3 ] ) );
-
-			expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' );
-			expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' );
-		} );
-
-		it( 'should throw if given range is not flat', () => {
-			const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) );
-
-			expect( () => {
-				doc.batch().move( notFlatRange, new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^batch-move-range-not-flat/ );
-		} );
-
-		it( 'should be chainable', () => {
-			chain = batch.move( div, new Position( root, [ 1, 3 ] ) );
-
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch.move( div, new Position( root, [ 2 ] ) );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'MoveDelta', () => {
 	let moveDelta, doc, root;
 

+ 0 - 71
packages/ckeditor5-engine/tests/model/delta/removedelta.js

@@ -3,79 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import { getNodesAndText } from '../../../tests/model/_utils/utils';
-import Document from '../../../src/model/document';
-import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
 import RemoveDelta from '../../../src/model/delta/removedelta';
 
-describe( 'Batch', () => {
-	let doc, root, div, p, batch, chain, range;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		div = new Element( 'div', [], new Text( 'foobar' ) );
-		p = new Element( 'p', [], new Text( 'abcxyz' ) );
-
-		div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
-		div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );
-
-		root.insertChildren( 0, [ div, p ] );
-
-		batch = doc.batch();
-
-		// Range starts in ROOT > DIV > P > gg|gg.
-		// Range ends in ROOT > DIV > ...|ar.
-		range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) );
-	} );
-
-	describe( 'remove', () => {
-		it( 'should remove specified node', () => {
-			batch.remove( div );
-
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.childCount ).to.equal( 1 );
-			expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
-		} );
-
-		it( 'should remove any range of nodes', () => {
-			batch.remove( range );
-
-			expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
-			expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
-		} );
-
-		it( 'should create minimal number of remove deltas, each with only one operation', () => {
-			batch.remove( range );
-
-			expect( batch.deltas.length ).to.equal( 2 );
-			expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 );
-			expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 );
-		} );
-
-		it( 'should be chainable', () => {
-			chain = batch.remove( range );
-
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch.remove( div );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'RemoveDelta', () => {
 	it( 'should provide proper className', () => {
 		expect( RemoveDelta.className ).to.equal( 'engine.model.delta.RemoveDelta' );

+ 0 - 46
packages/ckeditor5-engine/tests/model/delta/renamedelta.js

@@ -6,55 +6,9 @@
 import Document from '../../../src/model/document';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import RenameDelta from '../../../src/model/delta/renamedelta';
 
-describe( 'Batch', () => {
-	let doc, root, batch, chain;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		const p = new Element( 'p', null, new Text( 'abc' ) );
-		root.appendChildren( p );
-
-		batch = doc.batch();
-
-		chain = batch.rename( p, 'h' );
-	} );
-
-	describe( 'rename', () => {
-		it( 'should rename given element', () => {
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
-			expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
-		} );
-
-		it( 'should throw if not an Element instance is passed', () => {
-			expect( () => {
-				batch.rename( new Text( 'abc' ), 'h' );
-			} ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ );
-		} );
-
-		it( 'should be chainable', () => {
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch.rename( root.getChild( 0 ), 'p' );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.alwaysCalledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'RenameDelta', () => {
 	let renameDelta, doc, root;
 

+ 12 - 0
packages/ckeditor5-engine/tests/model/delta/rootattributedelta.js

@@ -0,0 +1,12 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import RootAttributeDelta from '../../../src/model/delta/rootattributedelta';
+
+describe( 'RootAttributeDelta', () => {
+	it( 'should provide proper className', () => {
+		expect( RootAttributeDelta.className ).to.equal( 'engine.model.delta.RootAttributeDelta' );
+	} );
+} );

+ 0 - 78
packages/ckeditor5-engine/tests/model/delta/splitdelta.js

@@ -6,8 +6,6 @@
 import Document from '../../../src/model/document';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import MergeDelta from '../../../src/model/delta/mergedelta';
 import SplitDelta from '../../../src/model/delta/splitdelta';
@@ -17,82 +15,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import NoOperation from '../../../src/model/operation/nooperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 
-import count from '@ckeditor/ckeditor5-utils/src/count';
-
-describe( 'Batch', () => {
-	let doc, root, p;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) );
-
-		root.insertChildren( 0, p );
-	} );
-
-	describe( 'split', () => {
-		it( 'should split foobar to foo and bar', () => {
-			doc.batch().split( new Position( root, [ 0, 3 ] ) );
-
-			expect( root.maxOffset ).to.equal( 2 );
-
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 3 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
-
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).maxOffset ).to.equal( 3 );
-			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' );
-		} );
-
-		it( 'should create an empty paragraph if we split at the end', () => {
-			doc.batch().split( new Position( root, [ 0, 6 ] ) );
-
-			expect( root.maxOffset ).to.equal( 2 );
-
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
-
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).maxOffset ).to.equal( 0 );
-			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-		} );
-
-		it( 'should throw if we try to split a root', () => {
-			expect( () => {
-				doc.batch().split( new Position( root, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^batch-split-root/ );
-		} );
-
-		it( 'should be chainable', () => {
-			const batch = doc.batch();
-
-			const chain = batch.split( new Position( root, [ 0, 3 ] ) );
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			const batch = doc.batch().split( new Position( root, [ 0, 3 ] ) );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'SplitDelta', () => {
 	let splitDelta, doc, root;
 

+ 0 - 50
packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js

@@ -4,10 +4,7 @@
  */
 
 import Document from '../../../src/model/document';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
 import Position from '../../../src/model/position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import UnwrapDelta from '../../../src/model/delta/unwrapdelta';
 import WrapDelta from '../../../src/model/delta/wrapdelta';
@@ -16,53 +13,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 
-describe( 'Batch', () => {
-	let doc, root, p;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		p = new Element( 'p', [], new Text( 'xyz' ) );
-		root.insertChildren( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] );
-	} );
-
-	describe( 'unwrap', () => {
-		it( 'should unwrap given element', () => {
-			doc.batch().unwrap( p );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'axyzb' );
-		} );
-
-		it( 'should throw if element to unwrap has no parent', () => {
-			const element = new Element( 'p' );
-
-			expect( () => {
-				doc.batch().unwrap( element );
-			} ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ );
-		} );
-
-		it( 'should be chainable', () => {
-			const batch = doc.batch();
-
-			const chain = batch.unwrap( p );
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			const batch = doc.batch().unwrap( p );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'UnwrapDelta', () => {
 	let unwrapDelta, doc, root;
 

+ 0 - 50
packages/ckeditor5-engine/tests/model/delta/weakinsertdelta.js

@@ -3,58 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
-import Position from '../../../src/model/position';
-import Text from '../../../src/model/text';
 import WeakInsertDelta from '../../../src/model/delta/weakinsertdelta';
 
-describe( 'Batch', () => {
-	let doc, root, batch, chain, attrs;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		root.insertChildren( 0, new Text( 'abc' ) );
-
-		batch = doc.batch();
-
-		attrs = [ [ 'bold', true ], [ 'foo', 'bar' ] ];
-
-		doc.selection.setAttributesTo( attrs );
-
-		chain = batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' );
-	} );
-
-	describe( 'weakInsert', () => {
-		it( 'should insert given nodes at given position', () => {
-			expect( root.maxOffset ).to.equal( 6 );
-			expect( root.getChild( 0 ).data ).to.equal( 'ab' );
-			expect( root.getChild( 1 ).data ).to.equal( 'xyz' );
-			expect( root.getChild( 2 ).data ).to.equal( 'c' );
-		} );
-
-		it( 'should set inserted nodes attributes to same as current selection attributes', () => {
-			expect( Array.from( root.getChild( 1 ).getAttributes() ) ).to.deep.equal( attrs );
-		} );
-
-		it( 'should be chainable', () => {
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'WeakInsertDelta', () => {
 	it( 'should provide proper className', () => {
 		expect( WeakInsertDelta.className ).to.equal( 'engine.model.delta.WeakInsertDelta' );

+ 0 - 83
packages/ckeditor5-engine/tests/model/delta/wrapdelta.js

@@ -5,10 +5,7 @@
 
 import Document from '../../../src/model/document';
 import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
 import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import WrapDelta from '../../../src/model/delta/wrapdelta';
 import UnwrapDelta from '../../../src/model/delta/unwrapdelta';
@@ -17,86 +14,6 @@ import InsertOperation from '../../../src/model/operation/insertoperation';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 
-describe( 'Batch', () => {
-	let doc, root, range;
-
-	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
-
-		root.insertChildren( 0, new Text( 'foobar' ) );
-
-		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-	} );
-
-	describe( 'wrap', () => {
-		it( 'should wrap flat range with given element', () => {
-			const p = new Element( 'p' );
-			doc.batch().wrap( range, p );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
-			expect( root.getChild( 1 ) ).to.equal( p );
-			expect( p.getChild( 0 ).data ).to.equal( 'ob' );
-			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
-		} );
-
-		it( 'should wrap flat range with an element of given name', () => {
-			doc.batch().wrap( range, 'p' );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' );
-			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
-		} );
-
-		it( 'should throw if range to wrap is not flat', () => {
-			root.insertChildren( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] );
-			const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) );
-
-			expect( () => {
-				doc.batch().wrap( notFlatRange, 'p' );
-			} ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ );
-		} );
-
-		it( 'should throw if element to wrap with has children', () => {
-			const p = new Element( 'p', [], new Text( 'a' ) );
-
-			expect( () => {
-				doc.batch().wrap( range, p );
-			} ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ );
-		} );
-
-		it( 'should throw if element to wrap with has children', () => {
-			const p = new Element( 'p' );
-			root.insertChildren( 0, p );
-
-			expect( () => {
-				doc.batch().wrap( range, p );
-			} ).to.throw( CKEditorError, /^batch-wrap-element-attached/ );
-		} );
-
-		it( 'should be chainable', () => {
-			const batch = doc.batch();
-
-			const chain = batch.wrap( range, 'p' );
-			expect( chain ).to.equal( batch );
-		} );
-
-		it( 'should add delta to batch and operation to delta before applying operation', () => {
-			sinon.spy( doc, 'applyOperation' );
-			const batch = doc.batch().wrap( range, 'p' );
-
-			const correctDeltaMatcher = sinon.match( operation => {
-				return operation.delta && operation.delta.batch && operation.delta.batch == batch;
-			} );
-
-			expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
-		} );
-	} );
-} );
-
 describe( 'WrapDelta', () => {
 	let wrapDelta, doc, root;
 

+ 34 - 2
packages/ckeditor5-engine/tests/model/document/document.js

@@ -110,7 +110,8 @@ describe( 'Document', () => {
 	} );
 
 	describe( 'applyOperation()', () => {
-		it( 'should increase document version, execute operation and fire event with proper data', () => {
+		it( 'should increase document version, execute operation and fire event with proper data ' +
+			'when operation is a document operation', () => {
 			const changeCallback = sinon.spy();
 			const type = 't';
 			const data = { data: 'x' };
@@ -121,6 +122,7 @@ describe( 'Document', () => {
 			const operation = {
 				type,
 				baseVersion: 0,
+				isDocumentOperation: true,
 				_execute: sinon.stub().returns( data )
 			};
 
@@ -131,6 +133,7 @@ describe( 'Document', () => {
 			doc.applyOperation( operation );
 
 			expect( doc.version ).to.equal( 1 );
+			expect( doc.history._deltas.length ).to.equal( 1 );
 			sinon.assert.calledOnce( operation._execute );
 
 			sinon.assert.calledOnce( changeCallback );
@@ -140,6 +143,35 @@ describe( 'Document', () => {
 			expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type );
 		} );
 
+		it( 'should execute operation, not fire event and not increase document version ' +
+			'when operation is not a document operation', () => {
+			const changeCallback = sinon.spy();
+			const type = 't';
+			const data = { data: 'x' };
+			const batch = new Batch();
+			const delta = new Delta();
+			delta.type = 'type';
+
+			const operation = {
+				type,
+				baseVersion: 0,
+				isDocumentOperation: false,
+				_execute: sinon.stub().returns( data )
+			};
+
+			delta.addOperation( operation );
+			batch.addDelta( delta );
+
+			doc.on( 'change', changeCallback );
+			doc.applyOperation( operation );
+
+			expect( doc.version ).to.equal( 0 );
+			expect( doc.history._deltas.length ).to.equal( 0 );
+			sinon.assert.calledOnce( operation._execute );
+
+			sinon.assert.notCalled( changeCallback );
+		} );
+
 		it( 'should throw an error on the operation base version and the document version is different', () => {
 			const operation = {
 				baseVersion: 1
@@ -149,7 +181,7 @@ describe( 'Document', () => {
 				() => {
 					doc.applyOperation( operation );
 				}
-			).to.throw( CKEditorError, /model-document-applyOperation-wrong-version/ );
+			).to.throw( CKEditorError, /^model-document-applyOperation-wrong-version/ );
 		} );
 	} );
 

+ 5 - 5
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -1012,7 +1012,7 @@ describe( 'DocumentSelection', () => {
 					batchTypes.set( batch, batch.type );
 				} );
 
-				doc.batch().insert( rangeInEmptyP.start, 'x' );
+				doc.batch().insertText( 'x', rangeInEmptyP.start );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 				expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
@@ -1024,7 +1024,7 @@ describe( 'DocumentSelection', () => {
 				selection.setRanges( [ rangeInEmptyP ] );
 				selection.setAttribute( 'foo', 'bar' );
 
-				doc.batch().move( fullP.getChild( 0 ), rangeInEmptyP.start );
+				doc.batch().move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 			} );
@@ -1048,7 +1048,7 @@ describe( 'DocumentSelection', () => {
 
 				selection.setRanges( [ rangeInFullP ] );
 
-				doc.batch().insert( rangeInEmptyP.start, 'x' );
+				doc.batch().insertText( 'x', rangeInEmptyP.start );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 			} );
@@ -1076,7 +1076,7 @@ describe( 'DocumentSelection', () => {
 				selection.setAttribute( 'foo', 'bar' );
 
 				doc.enqueueChanges( () => {
-					doc.batch().insert( rangeInEmptyP.start, 'x' );
+					doc.batch().insertText( 'x', rangeInEmptyP.start );
 
 					// `emptyP` still has the attribute, because attribute clearing is in enqueued block.
 					expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.true;
@@ -1109,7 +1109,7 @@ describe( 'DocumentSelection', () => {
 
 				sinon.spy( doc, 'enqueueChanges' );
 
-				doc.batch( 'transparent' ).insert( rangeInEmptyP.start, 'x' );
+				doc.batch( 'transparent' ).insertText( 'x', rangeInEmptyP.start );
 
 				expect( doc.enqueueChanges.called ).to.be.false;
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );

+ 1 - 1
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -225,7 +225,7 @@ describe( 'Marker', () => {
 		expect( marker.getEnd().isEqual( range.end ) ).to.be.true;
 
 		doc.enqueueChanges( () => {
-			doc.batch().insert( Position.createAt( root, 0 ), 'abc' );
+			doc.batch().insertText( 'abc', root );
 		} );
 
 		const updatedRange = Range.createFromParentsAndOffsets( root, 4, root, 5 );

+ 29 - 0
packages/ckeditor5-engine/tests/model/operation/attributeoperation.js

@@ -59,6 +59,35 @@ describe( 'AttributeOperation', () => {
 		} );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should return true when attribute is applied on attached items', () => {
+			const op = new AttributeOperation(
+				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
+				'key',
+				'oldValue',
+				'newValue',
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should return false when attribute is applied on detached items', () => {
+			const docFrag = doc.batch().createDocumentFragment();
+			doc.batch().appendText( 'abc', null, docFrag );
+
+			const op = new AttributeOperation(
+				Range.createIn( docFrag ),
+				'key',
+				'oldValue',
+				'newValue',
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.false;
+		} );
+	} );
+
 	it( 'should insert attribute to the set of nodes', () => {
 		root.insertChildren( 0, new Text( 'bar' ) );
 

+ 55 - 0
packages/ckeditor5-engine/tests/model/operation/detachoperation.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Document from '../../../src/model/document';
+import DetachOperation from '../../../src/model/operation/detachoperation';
+import { wrapInDelta } from '../../../tests/model/_utils/utils';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import Position from '../../../src/model/position';
+
+describe( 'DetachOperation', () => {
+	let doc, batch, docFrag, element;
+
+	beforeEach( () => {
+		doc = new Document();
+		batch = doc.batch();
+
+		docFrag = batch.createDocumentFragment();
+		element = batch.createElement( 'element' );
+		batch.append( element, docFrag );
+	} );
+
+	it( 'should have type equal to detach', () => {
+		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
+
+		expect( op.type ).to.equal( 'detach' );
+	} );
+
+	it( 'should remove given element from parent', () => {
+		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
+
+		doc.applyOperation( wrapInDelta( op ) );
+
+		expect( docFrag.childCount ).to.equal( 0 );
+	} );
+
+	it( 'should throw when is executed on element from document', () => {
+		const root = doc.createRoot();
+		const element = batch.createElement( 'element' );
+		batch.append( element, root );
+
+		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
+
+		expect( () => {
+			op._execute();
+		} ).to.throw( CKEditorError, /^detach-operation-on-document-node/ );
+	} );
+
+	it( 'should be not a document operation', () => {
+		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
+
+		expect( op.isDocumentOperation ).to.false;
+	} );
+} );

+ 24 - 0
packages/ckeditor5-engine/tests/model/operation/insertoperation.js

@@ -206,6 +206,30 @@ describe( 'InsertOperation', () => {
 		expect( op2.nodes.getNode( 0 ) ).not.to.equal( text );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should return true when element is inserted to the document', () => {
+			const op = new InsertOperation(
+				new Position( root, [ 0 ] ),
+				new Text( 'x' ),
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should return false when element is inserted to document fragment', () => {
+			const docFrag = doc.batch().createDocumentFragment();
+
+			const op = new InsertOperation(
+				new Position( docFrag, [ 0 ] ),
+				new Text( 'x' ),
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.false;
+		} );
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
 			const position = new Position( root, [ 0 ] );

+ 20 - 0
packages/ckeditor5-engine/tests/model/operation/markeroperation.js

@@ -161,6 +161,26 @@ describe( 'MarkerOperation', () => {
 		expect( clone ).to.deep.equal( op );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should return true when new marker range is added to the document', () => {
+			const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version );
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should return false when marker range is removed from the document', () => {
+			const op = new MarkerOperation( 'name', range, null, doc.markers, doc.version );
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should return true when non-existing marker range is removed from the document', () => {
+			const op = new MarkerOperation( 'name', null, null, doc.markers, doc.version );
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper serialized object', () => {
 			const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version );

+ 28 - 0
packages/ckeditor5-engine/tests/model/operation/moveoperation.js

@@ -267,6 +267,34 @@ describe( 'MoveOperation', () => {
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should return root when operation is executed on attached items', () => {
+			const op = new MoveOperation(
+				new Position( root, [ 0, 0 ] ),
+				1,
+				new Position( root, [ 1, 0 ] ),
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should return false when operation is executed on detached items', () => {
+			const docFrag = doc.batch().createDocumentFragment();
+
+			doc.batch().appendText( 'abc', null, docFrag );
+
+			const op = new MoveOperation(
+				new Position( docFrag, [ 0 ] ),
+				1,
+				new Position( docFrag, [ 2 ] ),
+				doc.version
+			);
+
+			expect( op.isDocumentOperation ).to.false;
+		} );
+	} );
+
 	describe( 'getMovedRangeStart', () => {
 		it( 'should return move operation target position transformed by removing move operation source range', () => {
 			const sourcePosition = new Position( root, [ 0, 2 ] );

+ 4 - 0
packages/ckeditor5-engine/tests/model/operation/nooperation.js

@@ -37,6 +37,10 @@ describe( 'NoOperation', () => {
 		expect( clone.baseVersion ).to.equal( 0 );
 	} );
 
+	it( 'should be a document operation', () => {
+		expect( noop.isDocumentOperation ).to.true;
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
 			const serialized = jsonParseStringify( noop );

+ 35 - 0
packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js

@@ -10,6 +10,7 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'ReinsertOperation', () => {
@@ -99,6 +100,40 @@ describe( 'ReinsertOperation', () => {
 		expect( graveyard.maxOffset ).to.equal( 2 );
 	} );
 
+	it( 'should be a document operation', () => {
+		expect( operation.isDocumentOperation ).to.true;
+	} );
+
+	it( 'should throw when target position is not in the document', () => {
+		const docFrag = doc.batch().createDocumentFragment();
+
+		operation = new ReinsertOperation(
+			graveyardPosition,
+			1,
+			Position.createAt( docFrag ),
+			doc.version
+		);
+
+		expect( () => {
+			operation._execute();
+		} ).to.throw( CKEditorError, /^reinsert-operation-to-detached-parent/ );
+	} );
+
+	it( 'should throw when source position is not in the document', () => {
+		const docFrag = doc.batch().createDocumentFragment();
+
+		operation = new ReinsertOperation(
+			Position.createAt( docFrag ),
+			1,
+			rootPosition,
+			doc.version
+		);
+
+		expect( () => {
+			operation._execute();
+		} ).to.throw( CKEditorError, /^reinsert-operation-on-detached-item/ );
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
 			const serialized = jsonParseStringify( operation );

+ 31 - 0
packages/ckeditor5-engine/tests/model/operation/removeoperation.js

@@ -10,6 +10,7 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
 import Text from '../../../src/model/text';
 import Element from '../../../src/model/element';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'RemoveOperation', () => {
@@ -140,6 +141,36 @@ describe( 'RemoveOperation', () => {
 		expect( doc.graveyard.getChild( 2 ).name ).to.equal( 'y' );
 	} );
 
+	it( 'should throw when is executed on detached item', () => {
+		const batch = doc.batch();
+		const docFrag = batch.createDocumentFragment();
+		const item = batch.createElement( 'foo' );
+
+		batch.append( item, docFrag );
+
+		const op = new RemoveOperation(
+			new Position( docFrag, [ 0 ] ),
+			1,
+			new Position( doc.graveyard, [ 0 ] ),
+			doc.version
+		);
+
+		expect( () => {
+			op._execute();
+		} ).to.throw( CKEditorError, /^remove-operation-on-detached-item/ );
+	} );
+
+	it( 'should always be a document operation', () => {
+		const op = new RemoveOperation(
+			new Position( root, [ 2 ] ),
+			2,
+			new Position( doc.graveyard, [ 0 ] ),
+			doc.version
+		);
+
+		expect( op.isDocumentOperation ).to.true;
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
 			const op = new RemoveOperation(

+ 19 - 0
packages/ckeditor5-engine/tests/model/operation/renameoperation.js

@@ -92,6 +92,25 @@ describe( 'RenameOperation', () => {
 		expect( clone.newName ).to.equal( newName );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should be true when target item is in the document', () => {
+			const op = new RenameOperation( position, oldName, newName, doc.version );
+
+			expect( op.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should be false when target item is not in the document', () => {
+			const batch = doc.batch();
+			const docFrag = batch.createDocumentFragment();
+
+			batch.appendElement( 'element', null, docFrag );
+
+			const op = new RenameOperation( Position.createAt( docFrag ), oldName, newName, doc.version );
+
+			expect( op.isDocumentOperation ).to.false;
+		} );
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper serialized object', () => {
 			const op = new RenameOperation( Position.createAt( root, 'end' ), oldName, newName, doc.version );

+ 28 - 0
packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js

@@ -54,6 +54,34 @@ describe( 'RootAttributeOperation', () => {
 		} );
 	} );
 
+	describe( 'isDocumentOperation', () => {
+		it( 'should be true when root is in the document', () => {
+			const operation = new RootAttributeOperation(
+				root,
+				'isNew',
+				null,
+				true,
+				doc.version
+			);
+
+			expect( operation.isDocumentOperation ).to.true;
+		} );
+
+		it( 'should be false when root is not in the document', () => {
+			const element = doc.batch().createElement( 'element' );
+
+			const operation = new RootAttributeOperation(
+				element,
+				'isNew',
+				null,
+				true,
+				doc.version
+			);
+
+			expect( operation.isDocumentOperation ).to.false;
+		} );
+	} );
+
 	it( 'should add attribute on the root element', () => {
 		doc.applyOperation( wrapInDelta(
 			new RootAttributeOperation(

+ 4 - 27
packages/ckeditor5-engine/tests/model/schema/schema.js

@@ -793,13 +793,6 @@ describe( 'Schema', () => {
 			} );
 
 			it( 'should filter out disallowed attributes from given nodes', () => {
-				schema.removeDisallowedAttributes( [ text, image ], '$root' );
-
-				expect( Array.from( text.getAttributeKeys() ) ).to.deep.equal( [ 'a' ] );
-				expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'b' ] );
-			} );
-
-			it( 'should filter out disallowed attributes from given nodes (batch)', () => {
 				const root = doc.getRoot();
 				const batch = doc.batch();
 
@@ -834,22 +827,6 @@ describe( 'Schema', () => {
 				div = new Element( 'div', [], [ paragraph, bar, imageInDiv ] );
 			} );
 
-			it( 'should filter out disallowed attributes from child nodes', () => {
-				schema.removeDisallowedAttributes( [ div ], '$root' );
-
-				expect( stringify( div ) )
-					.to.equal(
-						'<div>' +
-							'<paragraph>' +
-								'<$text b="1">foo</$text>' +
-								'<image b="1"></image>' +
-							'</paragraph>' +
-							'<$text a="1">bar</$text>' +
-							'<image a="1"></image>' +
-						'</div>'
-					);
-			} );
-
 			it( 'should filter out disallowed attributes from child nodes (batch)', () => {
 				const root = doc.getRoot();
 				const batch = doc.batch();
@@ -893,21 +870,21 @@ describe( 'Schema', () => {
 			} );
 
 			it( 'should accept iterable as nodes', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), '$root' );
+				schema.removeDisallowedAttributes( frag.getChildren(), '$root', doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
 			} );
 
 			it( 'should accept Position as inside', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ) );
+				schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ), doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
 			} );
 
 			it( 'should accept Node as inside', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), [ root ] );
+				schema.removeDisallowedAttributes( frag.getChildren(), [ root ], doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
@@ -920,7 +897,7 @@ describe( 'Schema', () => {
 
 			const image = new Element( 'image', { a: 1, b: 1 } );
 
-			schema.removeDisallowedAttributes( [ image ], '$root' );
+			schema.removeDisallowedAttributes( [ image ], '$root', doc.batch() );
 
 			expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'a', 'b' ] );
 		} );

+ 4 - 4
packages/ckeditor5-engine/tests/model/selection.js

@@ -894,14 +894,14 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return selected element', () => {
-			const { selection, model } = parse( '<p>foo</p>[<p>bar</p>]<p>baz</p>', schema );
+			const { selection, model } = parse( '<p>foo</p>[<p>bar</p>]<p>baz</p>', schema, doc.batch() );
 			const p = model.getChild( 1 );
 
 			expect( selection.getSelectedElement() ).to.equal( p );
 		} );
 
 		it( 'should return null if there is more than one range', () => {
-			const { selection } = parse( '[<p>foo</p>][<p>bar</p>]<p>baz</p>', schema );
+			const { selection } = parse( '[<p>foo</p>][<p>bar</p>]<p>baz</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
@@ -911,13 +911,13 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return null if selection is not over single element #1', () => {
-			const { selection } = parse( '<p>foo</p>[<p>bar</p><p>baz}</p>', schema );
+			const { selection } = parse( '<p>foo</p>[<p>bar</p><p>baz}</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
 
 		it( 'should return null if selection is not over single element #2', () => {
-			const { selection } = parse( '<p>{bar}</p>', schema );
+			const { selection } = parse( '<p>{bar}</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );