8
0
Просмотр исходного кода

Merge pull request #304 from ckeditor/t/173

treeView.DocumentFragment
Piotr Jasiun 9 лет назад
Родитель
Сommit
b6c8f4df2e

+ 125 - 0
packages/ckeditor5-engine/src/treeview/documentfragment.js

@@ -0,0 +1,125 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utils from '../../utils/utils.js';
+
+/**
+ * DocumentFragment class.
+ *
+ * @memberOf engine.treeView
+ */
+export default class DocumentFragment {
+	/**
+	 * Creates new DocumentFragment instance.
+	 *
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} [children] List of nodes to be inserted into
+	 * created document fragment.
+	 */
+	constructor( children ) {
+		/**
+		 * Array of child nodes.
+		 *
+		 * @protected
+		 * @member {Array.<engine.treeView.Element>} engine.treeView.DocumentFragment#_children
+		 */
+		this._children = [];
+
+		if ( children ) {
+			this.insertChildren( 0, children );
+		}
+	}
+
+	/**
+	 * {@link engine.treeView.DocumentFragment#insertChildren Insert} a child node or a list of child nodes at the end
+	 * and sets the parent of these nodes to this fragment.
+	 *
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or the list of nodes to be inserted.
+	 * @returns {Number} Number of appended nodes.
+	 */
+	appendChildren( nodes ) {
+		return this.insertChildren( this.getChildCount(), nodes );
+	}
+
+	/**
+	 * Gets child at the given index.
+	 *
+	 * @param {Number} index Index of child.
+	 * @returns {engine.treeView.Node} Child node.
+	 */
+	getChild( index ) {
+		return this._children[ index ];
+	}
+
+	/**
+	 * Gets the number of elements in fragment.
+	 *
+	 * @returns {Number} The number of elements.
+	 */
+	getChildCount() {
+		return this._children.length;
+	}
+
+	/**
+	 * Gets index of the given child node. Returns `-1` if child node is not found.
+	 *
+	 * @param {engine.treeView.Node} node Child node.
+	 * @returns {Number} Index of the child node.
+	 */
+	getChildIndex( node ) {
+		return this._children.indexOf( node );
+	}
+
+	/**
+	 * Gets child nodes iterator.
+	 *
+	 * @returns {Iterable.<engine.treeView.Node>} Child nodes iterator.
+	 */
+	getChildren() {
+		return this._children[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
+	 * this fragment.
+	 *
+	 * @param {Number} index Position where nodes should be inserted.
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or list of nodes to be inserted.
+	 * @returns {Number} Number of inserted nodes.
+	 */
+	insertChildren( index, nodes ) {
+		let count = 0;
+
+		if ( !utils.isIterable( nodes ) ) {
+			nodes = [ nodes ];
+		}
+
+		for ( let node of nodes ) {
+			node.parent = this;
+
+			this._children.splice( index, 0, node );
+			index++;
+			count++;
+		}
+
+		return count;
+	}
+
+	/**
+	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+	 *
+	 * @param {Number} index Number of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
+	 */
+	removeChildren( index, howMany = 1 ) {
+		for ( let i = index; i < index + howMany; i++ ) {
+			this._children[ i ].parent = null;
+		}
+
+		return this._children.splice( index, howMany );
+	}
+}

+ 106 - 34
packages/ckeditor5-engine/src/treeview/domconverter.js

@@ -7,6 +7,7 @@
 
 import ViewText from './text.js';
 import ViewElement from './element.js';
+import ViewDocumentFragment from './documentfragment.js';
 
 /**
  * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
@@ -52,8 +53,8 @@ export default class DomConverter {
 
 	/**
 	 * Binds DOM and View elements, so it will be possible to get corresponding elements using
-	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement} and
-	 * {@link engine.treeView.DomConverter#getCorespondingDOMElement}.
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement getCorrespondingViewElement} and
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomElement getCorrespondingDomElement}.
 	 *
 	 * @param {HTMLElement} domElement DOM element to bind.
 	 * @param {engine.treeView.Element} viewElement View element to bind.
@@ -63,6 +64,19 @@ export default class DomConverter {
 		this._viewToDomMapping.set( viewElement, domElement );
 	}
 
+	/**
+	 * Binds DOM and View document fragments, so it will be possible to get corresponding document fragments using
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment} and
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}.
+	 *
+	 * @param {DocumentFragment} domFragment DOM document fragment to bind.
+	 * @param {engine.treeView.DocumentFragment} viewFragment View document fragment to bind.
+	 */
+	bindDocumentFragments( domFragment, viewFragment ) {
+		this._domToViewMapping.set( domFragment, viewFragment );
+		this._viewToDomMapping.set( viewFragment, domFragment );
+	}
+
 	/**
 	 * Compares DOM and View nodes. Elements are same when they are bound. Text nodes are same when they have the same
 	 * text data. Nodes need to have corresponding types. In all other cases nodes are different.
@@ -86,15 +100,15 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts view to DOM. For all text nodes and not bound elements new elements will be created. For bound
-	 * elements function will return corresponding elements.
+	 * Converts view to DOM. For all text nodes, not bound elements and document fragments new items will
+	 * be created. For bound elements and document fragments function will return corresponding items.
 	 *
-	 * @param {engine.treeView.Node} viewNode View node to transform.
+	 * @param {engine.treeView.Node|engine.treeView.DocumentFragment} viewNode View node or document fragment to transform.
 	 * @param {document} domDocument Document which will be used to create DOM nodes.
 	 * @param {Object} [options] Conversion options.
 	 * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
-	 * @param {Boolean} [options.withChildren=true] If true node's children will be converted too.
-	 * @returns {Node} Converted node.
+	 * @param {Boolean} [options.withChildren=true] If true node's and document fragment's children  will be converted too.
+	 * @returns {Node|DocumentFragment} Converted node or DocumentFragment.
 	 */
 	viewToDom( viewNode, domDocument, options ) {
 		if ( !options ) {
@@ -108,14 +122,27 @@ export default class DomConverter {
 				return this.getCorrespondingDom( viewNode );
 			}
 
-			const domElement = domDocument.createElement( viewNode.name );
+			let domElement;
 
-			if ( options.bind ) {
-				this.bindElements( domElement, viewNode );
-			}
+			if ( viewNode instanceof ViewDocumentFragment ) {
+				// Create DOM document fragment.
+				domElement = domDocument.createDocumentFragment();
+
+				if ( options.bind ) {
+					this.bindDocumentFragments( domElement, viewNode );
+				}
+			} else {
+				// Create DOM element.
+				domElement = domDocument.createElement( viewNode.name );
+
+				if ( options.bind ) {
+					this.bindElements( domElement, viewNode );
+				}
 
-			for ( let key of viewNode.getAttributeKeys() ) {
-				domElement.setAttribute( key, viewNode.getAttribute( key ) );
+				// Copy element's attributes.
+				for ( let key of viewNode.getAttributeKeys() ) {
+					domElement.setAttribute( key, viewNode.getAttribute( key ) );
+				}
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
@@ -129,14 +156,14 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts DOM to view. For all text nodes and not bound elements new elements will be created. For bound
-	 * elements function will return corresponding elements.
+	 * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
+	 * be created. For bound elements and document fragments function will return corresponding items.
 	 *
-	 * @param {Node} domNode DOM node to transform.
+	 * @param {Node|DocumentFragment} domNode DOM node or document fragment to transform.
 	 * @param {Object} [options] Conversion options.
 	 * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
-	 * @param {Boolean} [options.withChildren=true] It true node's children will be converted too.
-	 * @returns {engine.treeView.Node} Converted node.
+	 * @param {Boolean} [options.withChildren=true] It true node's and document fragment's children will be converted too.
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment} Converted node or document fragment.
 	 */
 	domToView( domNode, options ) {
 		if ( !options ) {
@@ -150,16 +177,29 @@ export default class DomConverter {
 				return this.getCorrespondingView( domNode );
 			}
 
-			const viewElement = new ViewElement( domNode.tagName.toLowerCase() );
+			let viewElement;
 
-			if ( options.bind ) {
-				this.bindElements( domNode, viewElement );
-			}
+			if ( domNode instanceof  DocumentFragment ) {
+				// Create view document fragment.
+				viewElement = new ViewDocumentFragment();
 
-			const attrs = domNode.attributes;
+				if ( options.bind ) {
+					this.bindDocumentFragments( domNode, viewElement );
+				}
+			} else {
+				// Create view element.
+				viewElement = new ViewElement( domNode.tagName.toLowerCase() );
 
-			for ( let i = attrs.length - 1; i >= 0; i-- ) {
-				viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value );
+				if ( options.bind ) {
+					this.bindElements( domNode, viewElement );
+				}
+
+				// Copy element's attributes.
+				const attrs = domNode.attributes;
+
+				for ( let i = attrs.length - 1; i >= 0; i-- ) {
+					viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value );
+				}
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
@@ -175,15 +215,20 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding view node. This function use {@link engine.treeView.DomConverter#getCorrespondingViewElement}
-	 * for elements and {@link getCorrespondingViewText} for text nodes.
+	 * Gets corresponding view item. This function use
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement getCorrespondingViewElement}
+	 * for elements, {@link  engine.treeView.DomConverter#getCorrespondingViewText getCorrespondingViewText} for text
+	 * nodes and {@link engine.treeView.DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment}
+	 * for document fragments.
 	 *
-	 * @param {Node} domNode DOM node.
-	 * @returns {engine.treeView.Node|null} Corresponding node.
+	 * @param {Node|DocumentFragment} domNode DOM node or document fragment.
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment|null} Corresponding view item.
 	 */
 	getCorrespondingView( domNode ) {
 		if ( domNode instanceof HTMLElement ) {
 			return this.getCorrespondingViewElement( domNode );
+		} else if ( domNode instanceof DocumentFragment ) {
+			return this.getCorrespondingViewDocumentFragment( domNode );
 		} else {
 			return this.getCorrespondingViewText( domNode );
 		}
@@ -194,12 +239,23 @@ export default class DomConverter {
 	 * {@link engine.treeView.DomConverter#bindElements bound} to the given DOM element or null otherwise.
 	 *
 	 * @param {HTMLElement} domElement DOM element.
-	 * @returns {engine.treeView.Element|null} Corresponding element or null if none element was bound.
+	 * @returns {engine.treeView.Element|null} Corresponding element or null if no element was bound.
 	 */
 	getCorrespondingViewElement( domElement ) {
 		return this._domToViewMapping.get( domElement );
 	}
 
+	/**
+	 * Gets corresponding view document fragment. Returns document fragment if an view element was
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given DOM fragment or null otherwise.
+	 *
+	 * @param {DocumentFragment} domFragment DOM element.
+	 * @returns {engine.treeView.DocumentFragment|null} Corresponding document fragment or null if none element was bound.
+	 */
+	getCorrespondingViewDocumentFragment( domFragment ) {
+		return this._domToViewMapping.get( domFragment );
+	}
+
 	/**
 	 * Gets corresponding text node. Text nodes are not {@link engine.treeView.DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.
@@ -245,15 +301,20 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding DOM node. This function uses {@link engine.treeView.DomConverter#getCorrespondingDomElement} for
-	 * elements and {@link engine.treeView.DomConverter#getCorrespondingDomText} for text nodes.
+	 * Gets corresponding DOM item. This function uses
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomElement getCorrespondingDomElement} for
+	 * elements, {@link engine.treeView.DomConverter#getCorrespondingDomText getCorrespondingDomText} for text nodes
+	 * and {@link engine.treeView.DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}
+	 * for document fragments.
 	 *
-	 * @param {engine.treeView.Node} viewNode View node.
-	 * @returns {Node|null} Corresponding DOM node.
+	 * @param {engine.treeView.Node|engine.treeView.DomFragment} viewNode View node or document fragment.
+	 * @returns {Node|DocumentFragment|null} Corresponding DOM node or document fragment.
 	 */
 	getCorrespondingDom( viewNode ) {
 		if ( viewNode instanceof ViewElement ) {
 			return this.getCorrespondingDomElement( viewNode );
+		} else if ( viewNode instanceof ViewDocumentFragment ) {
+			return this.getCorrespondingDomDocumentFragment( viewNode );
 		} else {
 			return this.getCorrespondingDomText( viewNode );
 		}
@@ -270,6 +331,17 @@ export default class DomConverter {
 		return this._viewToDomMapping.get( viewElement );
 	}
 
+	/**
+	 * Gets corresponding DOM document fragment. Returns document fragment if an DOM element was
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given view document fragment or null otherwise.
+	 *
+	 * @param {engine.treeView.DocumentFragment} viewDocumentFragment View document fragment.
+	 * @returns {DocumentFragment|null} Corresponding document fragment or null if no fragment was bound.
+	 */
+	getCorrespondingDomDocumentFragment( viewDocumentFragment ) {
+		return this._viewToDomMapping.get( viewDocumentFragment );
+	}
+
 	/**
 	 * Gets corresponding text node. Text nodes are not {@link engine.treeView.DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.

+ 1 - 1
packages/ckeditor5-engine/src/treeview/node.js

@@ -26,7 +26,7 @@ export default class Node {
 		 * Parent element. Null by default. Set by {@link engine.treeView.Element#insertChildren}.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.Element|null} engine.treeView.Node#parent
+		 * @member {engine.treeView.Element|engine.treeView.DocumentFragment|null} engine.treeView.Node#parent
 		 */
 		this.parent = null;
 

+ 4 - 3
packages/ckeditor5-engine/src/treeview/writer.js

@@ -10,6 +10,7 @@ import Element from './element.js';
 import Text from './text.js';
 import Range from './range.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
+import DocumentFragment from './documentfragment.js';
 
 /**
  * Tree View Writer class.
@@ -352,7 +353,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 	 *
 	 * @param {engine.treeView.Range} range Range to remove from container. After removing, it will be updated
 	 * to a collapsed range showing the new position.
-	 * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
+	 * @returns {engine.treeView.DocumentFragment} Document fragment containing removed nodes.
 	 */
 	remove( range ) {
 		// Range should be placed inside one container.
@@ -367,7 +368,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 
 		// If range is collapsed - nothing to remove.
 		if ( range.isCollapsed ) {
-			return [];
+			return new DocumentFragment();
 		}
 
 		// Break attributes at range start and end.
@@ -385,7 +386,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 		range.end = Position.createFromPosition( mergePosition );
 
 		// Return removed nodes.
-		return removed;
+		return new DocumentFragment( removed );
 	}
 
 	/**

+ 199 - 0
packages/ckeditor5-engine/tests/treeview/documentfragment.js

@@ -0,0 +1,199 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview */
+
+'use strict';
+
+import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import Element from '/ckeditor5/engine/treeview/element.js';
+import Node from '/ckeditor5/engine/treeview/node.js';
+
+describe( 'DocumentFragment', () => {
+	describe( 'constructor', () => {
+		it( 'should create DocumentFragment without children', () => {
+			const fragment = new DocumentFragment();
+
+			expect( fragment ).to.be.an.instanceof( DocumentFragment );
+			expect( fragment.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create DocumentFragment with child node', () => {
+			const child = new Element( 'p' );
+			const fragment = new DocumentFragment( child );
+
+			expect( fragment.getChildCount() ).to.equal( 1 );
+			expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'p' );
+		} );
+
+		it( 'should create DocumentFragment with multiple nodes', () => {
+			const children = [ new Element( 'p' ), new Element( 'div' ) ];
+			const fragment = new DocumentFragment( children );
+
+			expect( fragment.getChildCount() ).to.equal( 2 );
+			expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'p' );
+			expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'div' );
+		} );
+	} );
+
+	describe( 'children manipulation methods', () => {
+		let fragment, el1, el2, el3, el4;
+
+		beforeEach( () => {
+			fragment = new DocumentFragment();
+			el1 = new Element( 'el1' );
+			el2 = new Element( 'el2' );
+			el3 = new Element( 'el3' );
+			el4 = new Element( 'el4' );
+		} );
+
+		describe( 'insertion', () => {
+			it( 'should insert children', () => {
+				const count1 = fragment.insertChildren( 0, [ el1, el3 ] );
+				const count2 = fragment.insertChildren( 1, el2 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el2' );
+				expect( fragment.getChild( 2 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+				expect( count1 ).to.equal( 2 );
+				expect( count2 ).to.equal( 1 );
+			} );
+
+			it( 'should append children', () => {
+				const count1 = fragment.insertChildren( 0, el1 );
+				const count2 = fragment.appendChildren( el2 );
+				const count3 = fragment.appendChildren( el3 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el2' );
+				expect( fragment.getChild( 2 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+				expect( count1 ).to.equal( 1 );
+				expect( count2 ).to.equal( 1 );
+				expect( count3 ).to.equal( 1 );
+			} );
+		} );
+
+		describe( 'getChildIndex', () => {
+			it( 'should return child index', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChildIndex( el1 ) ).to.equal( 0 );
+				expect( fragment.getChildIndex( el2 ) ).to.equal( 1 );
+				expect( fragment.getChildIndex( el3 ) ).to.equal( 2 );
+			} );
+		} );
+
+		describe( 'getChildren', () => {
+			it( 'should renturn children iterator', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				const expected = [ el1, el2, el3 ];
+				let i = 0;
+
+				for ( let child of fragment.getChildren() ) {
+					expect( child ).to.equal( expected[ i ] );
+					i++;
+				}
+
+				expect( i ).to.equal( 3 );
+			} );
+		} );
+
+		describe( 'removeChildren', () => {
+			it( 'should remove children', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+				fragment.appendChildren( el4 );
+
+				fragment.removeChildren( 1, 2 );
+
+				expect( fragment.getChildCount() ).to.equal( 2 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el4' );
+
+				expect( el1.parent ).to.equal( fragment );
+				expect( el2.parent ).to.be.null;
+				expect( el3.parent ).to.be.null;
+				expect( el4.parent ).equal( fragment );
+			} );
+
+			it( 'should remove one child when second parameter is not specified', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				const removed = fragment.removeChildren( 1 );
+
+				expect( fragment.getChildCount() ).to.equal( 2 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+
+				expect( removed.length ).to.equal( 1 );
+				expect( removed[ 0 ] ).to.have.property( 'name' ).that.equals( 'el2' );
+			} );
+		} );
+	} );
+
+	describe( 'node methods when inserted to fragment', () => {
+		it( 'getIndex() should return proper value', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			const fragment = new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getIndex() ).to.equal( 0 );
+			expect( node2.getIndex() ).to.equal( 1 );
+			expect( node3.getIndex() ).to.equal( 2 );
+			expect( node1.parent ).to.equal( fragment );
+			expect( node2.parent ).to.equal( fragment );
+			expect( node3.parent ).to.equal( fragment );
+		} );
+
+		it( 'getNextSibling() should return proper node', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getNextSibling() ).to.equal( node2 );
+			expect( node2.getNextSibling() ).to.equal( node3 );
+			expect( node3.getNextSibling() ).to.be.null;
+		} );
+
+		it( 'getPreviousSibling() should return proper node', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getPreviousSibling() ).to.be.null;
+			expect( node2.getPreviousSibling() ).to.equal( node1 );
+			expect( node3.getPreviousSibling() ).to.equal( node2 );
+		} );
+
+		it( 'remove() should remove node from fragment', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			const fragment = new DocumentFragment( [ node1, node2, node3 ] );
+
+			node1.remove();
+			node3.remove();
+
+			expect( fragment.getChildCount() ).to.equal( 1 );
+			expect( node1.parent ).to.be.null;
+			expect( node3.parent ).to.be.null;
+			expect( fragment.getChild( 0 ) ).to.equal( node2 );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-engine/tests/treeview/domconverter.js

@@ -11,6 +11,7 @@ import utils from '/ckeditor5/utils/utils.js';
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
 import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 
 describe( 'DomConverter', () => {
 	let converter;
@@ -31,6 +32,18 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'bindDocumentFragments', () => {
+		it( 'should bind document fragments', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
 	describe( 'compareNodes', () => {
 		it( 'should return false for nodes not matching types', () => {
 			const domElement = document.createElement( 'p' );
@@ -138,6 +151,56 @@ describe( 'DomConverter', () => {
 			expect( viewP.getChildCount() ).to.equal( 0 );
 			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
 		} );
+
+		it( 'should create view document fragment from DOM document fragment', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewFragment = converter.domToView( domFragment, { bind: true } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+			expect( viewFragment.getChildCount() ).to.equal( 2 );
+			expect( viewFragment.getChild( 0 ).name ).to.equal( 'img' );
+			expect( viewFragment.getChild( 1 ).data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.getCorrespondingDom( viewFragment.getChild( 0 ) ) ).to.equal( domFragment.childNodes[ 0 ] );
+		} );
+
+		it( 'should create view document fragment from DOM document fragment without children', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewFragment = converter.domToView( domFragment, { withChildren: false } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+
+			expect( viewFragment.getChildCount() ).to.equal( 0 );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.not.equal( domFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const viewFragment2 = converter.domToView( domFragment );
+
+			expect( viewFragment2 ).to.equal( viewFragment );
+		} );
 	} );
 
 	describe( 'viewToDom', () => {
@@ -222,6 +285,56 @@ describe( 'DomConverter', () => {
 			expect( domP.childNodes.length ).to.equal( 0 );
 			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
 		} );
+
+		it( 'should create DOM document fragment from view document fragment and bind elements', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { bind: true } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+			expect( domFragment.childNodes.length ).to.equal( 2 );
+			expect( domFragment.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domFragment.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingView( domFragment.childNodes[ 0 ] ) ).to.equal( viewFragment.getChild( 0 ) );
+		} );
+
+		it( 'should create DOM document fragment from view document without children', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domImg = document.createElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { withChildren: false } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+
+			expect( domFragment.childNodes.length ).to.equal( 0 );
+			expect( converter.getCorrespondingView( domFragment ) ).not.to.equal( viewFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const domFragment2 = converter.viewToDom( viewFragment );
+
+			expect( domFragment2 ).to.equal( domFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingView', () => {
@@ -247,6 +360,15 @@ describe( 'DomConverter', () => {
 
 			expect( converter.getCorrespondingView( domText ) ).to.equal( viewText );
 		} );
+
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingViewElement', () => {
@@ -260,6 +382,17 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'getCorrespondingViewDocumentFragment', () => {
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingViewDocumentFragment( domFragment ) ).to.equal( viewFragment );
+		} );
+	} );
+
 	describe( 'getCorrespondingViewText', () => {
 		it( 'should return corresponding view text based on sibling', () => {
 			const domImg = document.createElement( 'img' );
@@ -356,6 +489,15 @@ describe( 'DomConverter', () => {
 
 			expect( converter.getCorrespondingDom( viewText ) ).to.equal( domText );
 		} );
+
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingDomElement', () => {
@@ -369,6 +511,17 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'getCorrespondingDomDocumentFragment', () => {
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDomDocumentFragment( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
 	describe( 'getCorrespondingDomText', () => {
 		it( 'should return corresponding DOM text based on sibling', () => {
 			const domImg = document.createElement( 'img' );

+ 6 - 5
packages/ckeditor5-engine/tests/treeview/writer/remove.js

@@ -11,6 +11,7 @@ import Writer from '/ckeditor5/engine/treeview/writer.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
+import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import utils from '/tests/engine/treeview/writer/_utils/utils.js';
 
 describe( 'Writer', () => {
@@ -32,13 +33,13 @@ describe( 'Writer', () => {
 			} ).to.throw( 'treeview-writer-invalid-range-container' );
 		} );
 
-		it( 'should return empty array when range is collapsed', () => {
+		it( 'should return empty DocumentFragment when range is collapsed', () => {
 			const p = new Element( 'p' );
 			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
-			const nodes = writer.remove( range );
+			const fragment = writer.remove( range );
 
-			expect( nodes ).to.be.array;
-			expect( nodes.length ).to.equal( 0 );
+			expect( fragment ).to.be.instanceof( DocumentFragment );
+			expect( fragment.getChildCount() ).to.equal( 0 );
 			expect( range.isCollapsed ).to.be.true;
 		} );
 
@@ -63,7 +64,7 @@ describe( 'Writer', () => {
 			} );
 
 			// Test removed nodes.
-			test( writer, null, removed, [
+			test( writer, null, Array.from( removed.getChildren() ), [
 				{ instanceOf: Text, data: 'foobar' }
 			] );
 		} );