Przeglądaj źródła

Merge pull request #371 from ckeditor/t/368

Changes required by the enter feature
Szymon Cofalik 9 lat temu
rodzic
commit
b0d3fea230
23 zmienionych plików z 400 dodań i 102 usunięć
  1. 6 4
      packages/ckeditor5-engine/src/treemodel/composer/composer.js
  2. 17 11
      packages/ckeditor5-engine/src/treemodel/composer/deletecontents.js
  3. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js
  4. 11 10
      packages/ckeditor5-engine/src/treemodel/delta/basic-deltas.js
  5. 1 1
      packages/ckeditor5-engine/src/treemodel/delta/delta.js
  6. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js
  7. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js
  8. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/movedelta.js
  9. 62 0
      packages/ckeditor5-engine/src/treemodel/delta/renamedelta.js
  10. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js
  11. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js
  12. 0 5
      packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js
  13. 4 2
      packages/ckeditor5-engine/src/treemodel/document.js
  14. 28 0
      packages/ckeditor5-engine/src/treemodel/element.js
  15. 33 10
      packages/ckeditor5-engine/src/treemodel/position.js
  16. 6 3
      packages/ckeditor5-engine/src/treemodel/schema.js
  17. 16 2
      packages/ckeditor5-engine/tests/treemodel/composer/composer.js
  18. 40 15
      packages/ckeditor5-engine/tests/treemodel/composer/deletecontents.js
  19. 94 0
      packages/ckeditor5-engine/tests/treemodel/delta/renamedelta.js
  20. 8 1
      packages/ckeditor5-engine/tests/treemodel/document/document.js
  21. 20 0
      packages/ckeditor5-engine/tests/treemodel/element.js
  22. 45 5
      packages/ckeditor5-engine/tests/treemodel/position.js
  23. 9 3
      packages/ckeditor5-engine/tests/treemodel/schema/schema.js

+ 6 - 4
packages/ckeditor5-engine/src/treemodel/composer/composer.js

@@ -27,7 +27,7 @@ export default class Composer {
 	 * Creates an instance of the composer.
 	 */
 	constructor() {
-		this.on( 'deleteContents', ( evt, data ) => deleteContents( data.batch, data.selection ) );
+		this.on( 'deleteContents', ( evt, data ) => deleteContents( data.batch, data.selection, data.options ) );
 		this.on( 'modifySelection', ( evt, data ) => modifySelection( data.selection, data.options ) );
 	}
 
@@ -40,14 +40,15 @@ export default class Composer {
 	 * then that behavior should be implemented in the view's listener. At the same time, the table feature
 	 * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
 	 * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
-	 * That needs to be done in order to ensure that other features which use `deleteContents()` work well with tables.
+	 * That needs to be done in order to ensure that other features which use `deleteContents()` will work well with tables.
 	 *
 	 * @fires engine.treeModel.composer.Composer#deleteContents
 	 * @param {engine.treeModel.Batch} batch Batch to which deltas will be added.
 	 * @param {engine.treeModel.Selection} selection Selection of which the content should be deleted.
+	 * @param {Object} options See {@link engine.treeModel.composer.deleteContents}'s options.
 	 */
-	deleteContents( batch, selection ) {
-		this.fire( 'deleteContents', { batch, selection } );
+	deleteContents( batch, selection, options ) {
+		this.fire( 'deleteContents', { batch, selection, options } );
 	}
 
 	/**
@@ -73,6 +74,7 @@ utils.mix( Composer, EmitterMixin );
  * @param {Object} data
  * @param {engine.treeModel.Batch} data.batch
  * @param {engine.treeModel.Selection} data.selection
+ * @param {Object} data.options See {@link engine.treeModel.composer.deleteContents}'s options.
  */
 
 /**

+ 17 - 11
packages/ckeditor5-engine/src/treemodel/composer/deletecontents.js

@@ -15,8 +15,12 @@ import utils from '../../../utils/utils.js';
  * @method engine.treeModel.composer.deleteContents
  * @param {engine.treeModel.Batch} batch Batch to which the deltas will be added.
  * @param {engine.treeModel.Selection} selection Selection of which the content should be deleted.
+ * @param {Object} [options]
+ * @param {Boolean} [options.merge=false] Merge elements after removing the contents of the selection.
+ * For example, `<h>x[x</h><p>y]y</p>` will become: `<h>x^y</h>` with the option enabled
+ * and: `<h>x^</h><p>y</p>` without it.
  */
-export default function deleteContents( batch, selection ) {
+export default function deleteContents( batch, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
 		return;
 	}
@@ -35,16 +39,18 @@ export default function deleteContents( batch, selection ) {
 	// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
 	// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
 	// want to override that behavior anyway.
-	const endPath = endPos.path;
-	const mergeEnd = Math.min( startPos.path.length - 1, endPath.length - 1 );
-	let mergeDepth = utils.compareArrays( startPos.path, endPath );
-
-	if ( typeof mergeDepth == 'number' ) {
-		for ( ; mergeDepth < mergeEnd; mergeDepth++ ) {
-			const mergePath = startPos.path.slice( 0, mergeDepth );
-			mergePath.push( startPos.path[ mergeDepth ] + 1 );
-
-			batch.merge( new Position( endPos.root, mergePath ) );
+	if ( options.merge ) {
+		const endPath = endPos.path;
+		const mergeEnd = Math.min( startPos.path.length - 1, endPath.length - 1 );
+		let mergeDepth = utils.compareArrays( startPos.path, endPath );
+
+		if ( typeof mergeDepth == 'number' ) {
+			for ( ; mergeDepth < mergeEnd; mergeDepth++ ) {
+				const mergePath = startPos.path.slice( 0, mergeDepth );
+				mergePath.push( startPos.path[ mergeDepth ] + 1 );
+
+				batch.merge( new Position( endPos.root, mergePath ) );
+			}
 		}
 	}
 

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js

@@ -65,11 +65,6 @@ export default class AttributeDelta extends Delta {
 		return null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return AttributeDelta;
 	}

+ 11 - 10
packages/ckeditor5-engine/src/treemodel/delta/basic-deltas.js

@@ -10,13 +10,14 @@
 // which would already have all default deltas registered.
 
 // Import default suite of deltas so a feature have to include only Batch class file.
-import d1 from './insertdelta.js';
-import d2 from './weakinsertdelta.js';
-import d3 from './movedelta.js';
-import d4 from './removedelta.js';
-import d5 from './attributedelta.js';
-import d6 from './splitdelta.js';
-import d7 from './mergedelta.js';
-import d8 from './wrapdelta.js';
-import d9 from './unwrapdelta.js';
-/*jshint unused: false*/
+import d01 from './attributedelta.js';
+import d02 from './insertdelta.js';
+import d03 from './mergedelta.js';
+import d04 from './movedelta.js';
+import d05 from './removedelta.js';
+import d06 from './renamedelta.js';
+import d07 from './splitdelta.js';
+import d08 from './unwrapdelta.js';
+import d09 from './weakinsertdelta.js';
+import d10 from './wrapdelta.js';
+/*jshint unused: false*/

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/delta/delta.js

@@ -57,7 +57,7 @@ export default class Delta {
 	 * A class that will be used when creating reversed delta.
 	 *
 	 * @private
-	 * @type {Object}
+	 * @type {Function}
 	 */
 	get _reverseDeltaClass() {
 		return Delta;

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js

@@ -46,11 +46,6 @@ export default class InsertDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return RemoveDelta;
 	}

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js

@@ -44,11 +44,6 @@ export default class MergeDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @protected
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return SplitDelta;
 	}

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/movedelta.js

@@ -61,11 +61,6 @@ export default class MoveDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return MoveDelta;
 	}

+ 62 - 0
packages/ckeditor5-engine/src/treemodel/delta/renamedelta.js

@@ -0,0 +1,62 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Delta from './delta.js';
+import { register } from '../batch.js';
+import InsertOperation from '../operation/insertoperation.js';
+import RemoveOperation from '../operation/removeoperation.js';
+import MoveOperation from '../operation/moveoperation.js';
+import Element from '../element.js';
+import Position from '../position.js';
+
+/**
+ * To provide specific OT behavior and better collisions solving, the {@link engine.treeModel.Batch#rename Batch#rename} method
+ * uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods.
+ *
+ * @memberOf engine.treeModel.delta
+ */
+export default class RenameDelta extends Delta {
+	get _reverseDeltaClass() {
+		return RenameDelta;
+	}
+}
+
+function apply( batch, delta, operation ) {
+	batch.addDelta( delta );
+	delta.addOperation( operation );
+	batch.doc.applyOperation( operation );
+}
+
+/**
+ * Renames the given element.
+ *
+ * @chainable
+ * @method engine.treeModel.Batch#rename
+ * @param {String} newName New element name.
+ * @param {engine.treeModel.Element} element The element to rename.
+ */
+register( 'rename', function( newName, element ) {
+	const delta = new RenameDelta();
+	const newElement = new Element( newName );
+
+	apply(
+		this, delta,
+		new InsertOperation( Position.createAfter( element ), newElement, this.doc.version )
+	);
+
+	apply(
+		this, delta,
+		new MoveOperation( Position.createAt( element ), element.getChildCount(), Position.createAt( newElement ), this.doc.version )
+	);
+
+	apply(
+		this, delta,
+		new RemoveOperation( Position.createBefore( element ), 1, this.doc.version )
+	);
+
+	return this;
+} );

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js

@@ -57,11 +57,6 @@ export default class SplitDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return MergeDelta;
 	}

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js

@@ -40,11 +40,6 @@ export default class UnwrapDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return WrapDelta;
 	}

+ 0 - 5
packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js

@@ -65,11 +65,6 @@ export default class WrapDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
-	/**
-	 * @see engine.treeModel.delta.Delta#_reverseDeltaClass
-	 * @private
-	 * @type {Object}
-	 */
 	get _reverseDeltaClass() {
 		return UnwrapDelta;
 	}

+ 4 - 2
packages/ckeditor5-engine/src/treemodel/document.js

@@ -178,10 +178,12 @@ export default class Document {
 	 * Creates a new top-level root.
 	 *
 	 * @param {String|Symbol} rootName Unique root name.
-	 * @param {String} elementName Element name.
+	 * @param {String} [elementName='$root'] Element name. Defaults to `'$root'` which also have
+	 * some basic schema defined (`$block`s are allowed inside the `$root`). Make sure to define a proper
+	 * schema if you use a different name.
 	 * @returns {engine.treeModel.RootElement} Created root.
 	 */
-	createRoot( rootName, elementName ) {
+	createRoot( rootName, elementName = '$root' ) {
 		if ( this._roots.has( rootName ) ) {
 			/**
 			 * Root with specified name already exists.

+ 28 - 0
packages/ckeditor5-engine/src/treemodel/element.js

@@ -8,6 +8,7 @@
 import Node from './node.js';
 import NodeList from './nodelist.js';
 import DocumentFragment from './documentfragment.js';
+import Range from './range.js';
 import utils from '../../utils/utils.js';
 
 /**
@@ -169,4 +170,31 @@ export default class Element extends Node {
 	clearAttributes() {
 		this._attrs.clear();
 	}
+
+	/**
+	 * Checks whether element is empty (has no children).
+	 *
+	 * @returns {Boolean}
+	 */
+	isEmpty() {
+		return this.getChildCount() === 0;
+	}
+
+	/**
+	 * Gets the text content of the element. The return value is created by concatenating all
+	 * text nodes in this element and its descendants.
+	 *
+	 * @returns {String}
+	 */
+	getText() {
+		let text = '';
+
+		for ( let value of Range.createFromElement( this ) ) {
+			if ( value.type == 'TEXT' ) {
+				text += value.item.text;
+			}
+		}
+
+		return text;
+	}
 }

+ 33 - 10
packages/ckeditor5-engine/src/treemodel/position.js

@@ -5,7 +5,6 @@
 
 'use strict';
 
-import RootElement from './rootelement.js';
 import DocumentFragment from './documentfragment.js';
 import Element from './element.js';
 import last from '../../utils/lib/lodash/last.js';
@@ -22,11 +21,12 @@ export default class Position {
 	/**
 	 * Creates a position.
 	 *
-	 * @param {engine.treeModel.RootElement|engine.treeModel.DocumentFragment} root Root element for the position path.
+	 * @param {engine.treeModel.Element|engine.treeModel.DocumentFragment} root
+	 * Root of the position path. Element (most often a {@link engine.treeModel.RootElement}) or a document fragment.
 	 * @param {Array.<Number>} path Position path. See {@link engine.treeModel.Position#path} property for more information.
 	 */
 	constructor( root, path ) {
-		if ( !( root instanceof RootElement ) && !( root instanceof DocumentFragment ) ) {
+		if ( !( root instanceof Element ) && !( root instanceof DocumentFragment ) ) {
 			/**
 			 * Position root invalid.
 			 *
@@ -35,13 +35,6 @@ export default class Position {
 			throw new CKEditorError( 'position-root-invalid: Position root invalid.' );
 		}
 
-		/**
-		 * Root element for the position path.
-		 *
-		 * @member {engine.treeModel.RootElement|engine.treeModel.DocumentFragment} engine.treeModel.Position#root
-		 */
-		this.root = root;
-
 		if ( !( path instanceof Array ) || path.length === 0 ) {
 			/**
 			 * Position path must be an Array with at least one item.
@@ -52,6 +45,18 @@ export default class Position {
 			throw new CKEditorError( 'position-path-incorrect: Position path must be an Array with at least one item.', { path: path } );
 		}
 
+		// Normalize the root and path (if element was passed).
+		path = root.getPath().concat( path );
+		root = root.root;
+
+		/**
+		 * Root of the position path.
+		 *
+		 * @readonly
+		 * @member {engine.treeModel.Element|engine.treeModel.DocumentFragment} engine.treeModel.Position#root
+		 */
+		this.root = root;
+
 		/**
 		 * Position of the node it the tree. Must contain at least one item. For example:
 		 *
@@ -417,6 +422,24 @@ export default class Position {
 		}
 	}
 
+	/**
+	 * Whether position is at the beginning of its {@link engine.treeModel.Position#parent}.
+	 *
+	 * @returns {Boolean}
+	 */
+	isAtStart() {
+		return this.offset === 0;
+	}
+
+	/**
+	 * Whether position is at the end of its {@link engine.treeModel.Position#parent}.
+	 *
+	 * @returns {Boolean}
+	 */
+	isAtEnd() {
+		return this.offset == this.parent.getChildCount();
+	}
+
 	/**
 	 * Creates position at the given location. The location can be specified as:
 	 *

+ 6 - 3
packages/ckeditor5-engine/src/treemodel/schema.js

@@ -234,9 +234,11 @@ export class SchemaItem {
  *		editor.document.schema.allow( '$text', 'bold' );
  *
  * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
+ *
  * * `$inline` represents all inline elements,
  * * `$text` is a sub-group of `$inline` and represents text nodes,
- * * `$block` represents block elements.
+ * * `$block` represents block elements,
+ * * `$root` represents default editing roots (those that allow only `$block`s inside them).
  *
  * When registering an item it's possible to tell that this item should inherit from some other existing item.
  * E.g. `p` can inherit from `$block`, so whenever given attribute is allowed on the `$block` it will automatically be
@@ -266,11 +268,12 @@ export default class Schema {
 		this._extensionChains = new Map();
 
 		// Register some default abstract entities.
-		this.registerItem( '$inline' );
+		this.registerItem( '$root' );
 		this.registerItem( '$block' );
+		this.registerItem( '$inline' );
 		this.registerItem( '$text', '$inline' );
 
-		// Allow inline elements inside block elements.
+		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$inline', inside: '$block' } );
 	}
 

+ 16 - 2
packages/ckeditor5-engine/tests/treemodel/composer/composer.js

@@ -23,16 +23,30 @@ describe( 'Composer', () => {
 
 	describe( 'constructor', () => {
 		it( 'attaches deleteContents default listener', () => {
-			setData( document, 'main', '<p><selection>foo</selection>bar</p>' );
+			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
 
 			const batch = document.batch();
 
 			composer.fire( 'deleteContents', { batch, selection: document.selection } );
 
-			expect( getData( document, 'main' ) ).to.equal( '<p>bar</p>' );
+			expect( getData( document, 'main' ) ).to.equal( '<p>f</p><p>r</p>' );
 			expect( batch.deltas ).to.not.be.empty;
 		} );
 
+		it( 'attaches deleteContents default listener which passes options', () => {
+			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+
+			const batch = document.batch();
+
+			composer.fire( 'deleteContents', {
+				batch,
+				selection: document.selection,
+				options: { merge: true }
+			} );
+
+			expect( getData( document, 'main' ) ).to.equal( '<p>fr</p>' );
+		} );
+
 		it( 'attaches modifySelection default listener', () => {
 			setData( document, 'main', '<p>foo<selection />bar</p>' );
 

+ 40 - 15
packages/ckeditor5-engine/tests/treemodel/composer/deletecontents.js

@@ -77,9 +77,10 @@ describe( 'Delete utils', () => {
 			);
 
 			test(
-				'deletes a bunch of nodes',
+				'does not break things when option.merge passed',
 				'w<selection>x<img></img>y</selection>z',
-				'w<selection />z'
+				'w<selection />z',
+				{ merge: true }
 			);
 		} );
 
@@ -122,77 +123,101 @@ describe( 'Delete utils', () => {
 			test(
 				'do not merge when no need to',
 				'<p>x</p><p><selection>foo</selection></p><p>y</p>',
-				'<p>x</p><p><selection /></p><p>y</p>'
+				'<p>x</p><p><selection /></p><p>y</p>',
+				{ merge: true }
+			);
+
+			test(
+				'merges second element into the first one (same name)',
+				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
+				'<p>x</p><p>fo<selection />ar</p><p>y</p>',
+				{ merge: true }
+			);
+
+			test(
+				'does not merge second element into the first one (same name, !option.merge)',
+				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
+				'<p>x</p><p>fo<selection /></p><p>ar</p><p>y</p>'
 			);
 
 			test(
 				'merges second element into the first one (same name)',
 				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p>fo<selection />ar</p><p>y</p>'
+				'<p>x</p><p>fo<selection />ar</p><p>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges second element into the first one (different name)',
 				'<p>x</p><h1>fo<selection>o</h1><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>'
+				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges second element into the first one (different name, backward selection)',
 				'<p>x</p><h1>fo<selection backward>o</h1><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>'
+				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges second element into the first one (different attrs)',
 				'<p>x</p><p align="l">fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p align="l">fo<selection />ar</p><p>y</p>'
+				'<p>x</p><p align="l">fo<selection />ar</p><p>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges second element to an empty first element',
 				'<p>x</p><h1><selection></h1><p>fo</selection>o</p><p>y</p>',
-				'<p>x</p><h1><selection />o</h1><p>y</p>'
+				'<p>x</p><h1><selection />o</h1><p>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges elements when deep nested',
 				'<p>x<pchild>fo<selection>o</pchild></p><p><pchild>b</selection>ar</pchild>y</p>',
-				'<p>x<pchild>fo<selection />ar</pchild>y</p>'
+				'<p>x<pchild>fo<selection />ar</pchild>y</p>',
+				{ merge: true }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'merges elements when left end deep nested',
 				'<p>x<pchild>fo<selection>o</pchild></p><p>b</selection>ary</p>',
-				'<p>x<pchild>fo<selection /></pchild>ary</p>'
+				'<p>x<pchild>fo<selection /></pchild>ary</p>',
+				{ merge: true }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'merges elements when right end deep nested',
 				'<p>xfo<selection>o</p><p><pchild>b</selection>ar</pchild>y<img></img></p>',
-				'<p>xfo<selection /><pchild>ar</pchild>y<img></img></p>'
+				'<p>xfo<selection /><pchild>ar</pchild>y<img></img></p>',
+				{ merge: true }
 			);
 
 			test(
 				'merges elements when more content in the right branch',
 				'<p>xfo<selection>o</p><p>b</selection>a<pchild>r</pchild>y</p>',
-				'<p>xfo<selection />a<pchild>r</pchild>y</p>'
+				'<p>xfo<selection />a<pchild>r</pchild>y</p>',
+				{ merge: true }
 			);
 
 			test(
 				'leaves just one element when all selected',
 				'<h1><selection>x</h1><p>foo</p><p>y</selection></p>',
-				'<h1><selection /></h1>'
+				'<h1><selection /></h1>',
+				{ merge: true }
 			);
 		} );
 
-		function test( title, input, output ) {
+		function test( title, input, output, options ) {
 			it( title, () => {
 				setData( document, 'main', input );
 
-				deleteContents( document.batch(), document.selection );
+				deleteContents( document.batch(), document.selection, options );
 
 				expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
 			} );

+ 94 - 0
packages/ckeditor5-engine/tests/treemodel/delta/renamedelta.js

@@ -0,0 +1,94 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel, delta */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treemodel/document.js';
+import Element from '/ckeditor5/engine/treemodel/element.js';
+
+import RenameDelta from '/ckeditor5/engine/treemodel/delta/renamedelta.js';
+
+describe( 'Batch', () => {
+	let doc, root, batch, chain;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		const p = new Element( 'p', null, 'abc' );
+		root.appendChildren( p );
+
+		batch = doc.batch();
+
+		chain = batch.rename( 'h', p );
+	} );
+
+	describe( 'rename', () => {
+		it( 'should rename given element', () => {
+			expect( root.getChildCount() ).to.equal( 1 );
+			expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
+			expect( root.getChild( 0 ).getText() ).to.equal( 'abc' );
+		} );
+
+		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( 'p', root.getChild( 0 ) );
+
+			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;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		renameDelta = new RenameDelta();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create rename delta with no operations added', () => {
+			expect( renameDelta.operations.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'getReversed', () => {
+		it( 'should return instance of RenameDelta', () => {
+			let reversed = renameDelta.getReversed();
+
+			expect( reversed ).to.be.instanceof( RenameDelta );
+		} );
+
+		it( 'should return correct RenameDelta', () => {
+			root.appendChildren( new Element( 'p', null, 'abc' ) );
+
+			const batch = doc.batch();
+
+			batch.rename( 'h', root.getChild( 0 ) );
+
+			const reversed = batch.deltas[ 0 ].getReversed();
+
+			reversed.operations.forEach( operation => {
+				doc.applyOperation( operation );
+			} );
+
+			expect( root.getChildCount() ).to.equal( 1 );
+			expect( root.getChild( 0 ) ).to.have.property( 'name', 'p' );
+			expect( root.getChild( 0 ).getText() ).to.equal( 'abc' );
+		} );
+	} );
+} );

+ 8 - 1
packages/ckeditor5-engine/tests/treemodel/document/document.js

@@ -50,11 +50,18 @@ describe( 'Document', () => {
 
 	describe( 'createRoot', () => {
 		it( 'should create a new RootElement, add it to roots map and return it', () => {
-			let root = doc.createRoot( 'root', 'root' );
+			let root = doc.createRoot( 'root' );
 
 			expect( doc._roots.size ).to.equal( 2 );
 			expect( root ).to.be.instanceof( RootElement );
 			expect( root.getChildCount() ).to.equal( 0 );
+			expect( root ).to.have.property( 'name', '$root' );
+		} );
+
+		it( 'should create a new RootElement with the specified name', () => {
+			let root = doc.createRoot( 'root', 'foo' );
+
+			expect( root ).to.have.property( 'name', 'foo' );
 		} );
 
 		it( 'should throw an error when trying to create a second root with the same name', () => {

+ 20 - 0
packages/ckeditor5-engine/tests/treemodel/element.js

@@ -219,4 +219,24 @@ describe( 'Element', () => {
 			} );
 		} );
 	} );
+
+	describe( 'isEmpty', () => {
+		it( 'checks whether element has no children', () => {
+			expect( new Element( 'a' ).isEmpty() ).to.be.true;
+			expect( new Element( 'a', null, 'x' ).isEmpty() ).to.be.false;
+		} );
+	} );
+
+	describe( 'getText()', () => {
+		it( 'returns all text nodes', () => {
+			const el = new Element( 'p', null, [
+				new Element( 'p', null, 'abc' ),
+				'def',
+				new Element( 'p', null, 'ghi' ),
+				'jkl',
+			] );
+
+			expect( el.getText() ).to.equal( 'abcdefghijkl' );
+		} );
+	} );
 } );

+ 45 - 5
packages/ckeditor5-engine/tests/treemodel/position.js

@@ -10,6 +10,7 @@
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
+import Text from '/ckeditor5/engine/treemodel/text.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
@@ -64,9 +65,34 @@ describe( 'position', () => {
 		} );
 
 		it( 'should accept DocumentFragment as a root', () => {
-			expect( () => {
-				new Position( new DocumentFragment(), [ 0 ] );
-			} ).not.to.throw;
+			const frag = new DocumentFragment();
+			const pos = new Position( frag, [ 0 ] );
+
+			expect( pos ).to.have.property( 'root', frag );
+		} );
+
+		it( 'should accept detached Element as a root', () => {
+			const el = new Element( 'p' );
+			const pos = new Position( el, [ 0 ] );
+
+			expect( pos ).to.have.property( 'root', el );
+			expect( pos.path ).to.deep.equal( [ 0 ] );
+		} );
+
+		it( 'should normalize attached Element as a root', () => {
+			const pos = new Position( li1, [ 0, 2 ] );
+
+			expect( pos ).to.have.property( 'root', root );
+			expect( pos.isEqual( Position.createAt( li1, 0, 2 ) ) );
+		} );
+
+		it( 'should normalize Element from a detached branch as a root', () => {
+			const rootEl = new Element( 'p', null, [ new Element( 'a' ) ] );
+			const elA = rootEl.getChild( 0 );
+			const pos = new Position( elA, [ 0 ] );
+
+			expect( pos ).to.have.property( 'root', rootEl );
+			expect( pos.isEqual( Position.createAt( elA, 0 ) ) );
 		} );
 
 		it( 'should throw error if given path is incorrect', () => {
@@ -81,11 +107,11 @@ describe( 'position', () => {
 
 		it( 'should throw error if given root is invalid', () => {
 			expect( () => {
-				new Position();
+				new Position( new Text( 'a' ) );
 			} ).to.throw( CKEditorError, /position-root-invalid/ );
 
 			expect( () => {
-				new Position( new Element( 'p' ), [ 0 ] );
+				new Position();
 			} ).to.throw( CKEditorError, /position-root-invalid/ );
 		} );
 	} );
@@ -420,6 +446,20 @@ describe( 'position', () => {
 		} );
 	} );
 
+	describe( 'isAtStart', () => {
+		it( 'should return true if position is at the beginning of its parent', () => {
+			expect( new Position( root, [ 0 ] ).isAtStart() ).to.be.true;
+			expect( new Position( root, [ 1 ] ).isAtStart() ).to.be.false;
+		} );
+	} );
+
+	describe( 'isAtEnd', () => {
+		it( 'should return true if position is at the end of its parent', () => {
+			expect( new Position( root, [ root.getChildCount() ] ).isAtEnd() ).to.be.true;
+			expect( new Position( root, [ 0 ] ).isAtEnd() ).to.be.false;
+		} );
+	} );
+
 	describe( 'compareWith', () => {
 		it( 'should return SAME if positions are same', () => {
 			const position = new Position( root, [ 1, 2, 3 ] );

+ 9 - 3
packages/ckeditor5-engine/tests/treemodel/schema/schema.js

@@ -13,6 +13,9 @@ import Document from '/ckeditor5/engine/treemodel/document.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+
+testUtils.createSinonSandbox();
 
 let schema;
 
@@ -22,14 +25,17 @@ beforeEach( () => {
 
 describe( 'constructor', () => {
 	it( 'should register base items: inline, block, root', () => {
-		sinon.spy( Schema.prototype, 'registerItem' );
+		testUtils.sinon.spy( Schema.prototype, 'registerItem' );
 
 		schema = new Schema();
 
-		expect( schema.registerItem.calledWithExactly( '$inline', null ) );
+		expect( schema.registerItem.calledWithExactly( '$root', null ) );
 		expect( schema.registerItem.calledWithExactly( '$block', null ) );
+		expect( schema.registerItem.calledWithExactly( '$inline', null ) );
+	} );
 
-		Schema.prototype.registerItem.restore();
+	it( 'should allow block in root', () => {
+		expect( schema.check( { name: '$block', inside: [ '$root' ] } ) ).to.be.true;
 	} );
 
 	it( 'should allow inline in block', () => {