浏览代码

Merge pull request #989 from ckeditor/t/922

Fix: Prevent unbinding elements that are reused during rendering. Closes #922.

BREAKING CHANGE: Removed `Renderer#getCorrespondingDom()` and `Renderer#getCorrespondingView()` methods.
BREAKING CHANGE: Renamed `Renderer#getCorrespondingDomText()` method to  `Renderer#findCorrespondingDomText()` and `Renderer#getCorrespondingViewText()` to  `Renderer#findCorrespondingViewText()`.
BREAKING CHANGE: Merged `Renderer#getCorrespondingDomElement()` and `Renderer#getCorrespondingDomDocumentFragment()` into one method `Renderer#mapViewToDom()`.
BREAKING CHANGE: Merged `Renderer#getCorrespondingViewElement()` and `Renderer#getCorrespondingViewDocumentFragment()` into `Renderer#mapDomToView()`.
Piotrek Koszuliński 8 年之前
父节点
当前提交
fa26db5e49

+ 63 - 118
packages/ckeditor5-engine/src/view/domconverter.js

@@ -124,8 +124,8 @@ export default class DomConverter {
 
 	/**
 	 * Binds DOM and View elements, so it will be possible to get corresponding elements using
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingViewElement getCorrespondingViewElement} and
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomElement getCorrespondingDomElement}.
+	 * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
+	 * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
 	 *
 	 * @param {HTMLElement} domElement DOM element to bind.
 	 * @param {module:engine/view/element~Element} viewElement View element to bind.
@@ -157,8 +157,8 @@ export default class DomConverter {
 
 	/**
 	 * Binds DOM and View document fragments, so it will be possible to get corresponding document fragments using
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment} and
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}.
+	 * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
+	 * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
 	 *
 	 * @param {DocumentFragment} domFragment DOM document fragment to bind.
 	 * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment View document fragment to bind.
@@ -172,8 +172,8 @@ export default class DomConverter {
 	 * 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 {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} viewNode View node or document fragment to
-	 * transform.
+	 * @param {module:engine/view/node~Node|module:engine/view/documentfragment~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.
@@ -186,8 +186,8 @@ export default class DomConverter {
 
 			return domDocument.createTextNode( textData );
 		} else {
-			if ( this.getCorrespondingDom( viewNode ) ) {
-				return this.getCorrespondingDom( viewNode );
+			if ( this.mapViewToDom( viewNode ) ) {
+				return this.mapViewToDom( viewNode );
 			}
 
 			let domElement;
@@ -200,8 +200,7 @@ export default class DomConverter {
 					this.bindDocumentFragments( domElement, viewNode );
 				}
 			} else if ( viewNode.is( 'uiElement' ) ) {
-				// UIElement has it's own render() method.
-				// https://github.com/ckeditor/ckeditor5-engine/issues/799
+				// UIElement has its own render() method (see #799).
 				domElement = viewNode.render( domDocument );
 
 				if ( options.bind ) {
@@ -234,8 +233,9 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts children of the view element to DOM using {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
-	 * Additionally this method adds block {@link module:engine/view/filler filler} to the list of children, if needed.
+	 * Converts children of the view element to DOM using the
+	 * {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
+	 * Additionally, this method adds block {@link module:engine/view/filler filler} to the list of children, if needed.
 	 *
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElement Parent view element.
 	 * @param {Document} domDocument Document which will be used to create DOM nodes.
@@ -294,7 +294,7 @@ export default class DomConverter {
 		const viewParent = viewPosition.parent;
 
 		if ( viewParent.is( 'text' ) ) {
-			const domParent = this.getCorrespondingDomText( viewParent );
+			const domParent = this.findCorrespondingDomText( viewParent );
 
 			if ( !domParent ) {
 				// Position is in a view text node that has not been rendered to DOM yet.
@@ -313,7 +313,7 @@ export default class DomConverter {
 			let domParent, domBefore, domAfter;
 
 			if ( viewPosition.offset === 0 ) {
-				domParent = this.getCorrespondingDom( viewPosition.parent );
+				domParent = this.mapViewToDom( viewParent );
 
 				if ( !domParent ) {
 					// Position is in a view element that has not been rendered to DOM yet.
@@ -322,7 +322,11 @@ export default class DomConverter {
 
 				domAfter = domParent.childNodes[ 0 ];
 			} else {
-				domBefore = this.getCorrespondingDom( viewPosition.nodeBefore );
+				const nodeBefore = viewPosition.nodeBefore;
+
+				domBefore = nodeBefore.is( 'text' ) ?
+					this.findCorrespondingDomText( nodeBefore ) :
+					this.mapViewToDom( viewPosition.nodeBefore );
 
 				if ( !domBefore ) {
 					// Position is after a view element that has not been rendered to DOM yet.
@@ -382,8 +386,8 @@ export default class DomConverter {
 		} else if ( this.isComment( domNode ) ) {
 			return null;
 		} else {
-			if ( this.getCorrespondingView( domNode ) ) {
-				return this.getCorrespondingView( domNode );
+			if ( this.mapDomToView( domNode ) ) {
+				return this.mapDomToView( domNode );
 			}
 
 			let viewElement;
@@ -423,7 +427,8 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts children of the DOM element to view nodes using {@link module:engine/view/domconverter~DomConverter#domToView} method.
+	 * Converts children of the DOM element to view nodes using
+	 * the {@link module:engine/view/domconverter~DomConverter#domToView} method.
 	 * Additionally this method omits block {@link module:engine/view/filler filler}, if it exists in the DOM parent.
 	 *
 	 * @param {HTMLElement} domElement Parent DOM element.
@@ -521,7 +526,8 @@ export default class DomConverter {
 		}
 
 		// If position is somewhere inside UIElement - return position before that element.
-		const viewElement = this.getCorrespondingViewElement( domParent );
+		const viewElement = this.mapDomToView( domParent );
+
 		if ( viewElement && viewElement.is( 'uiElement' ) ) {
 			return ViewPosition.createBefore( viewElement );
 		}
@@ -531,7 +537,7 @@ export default class DomConverter {
 				return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
 			}
 
-			const viewParent = this.getCorrespondingViewText( domParent );
+			const viewParent = this.findCorrespondingViewText( domParent );
 			let offset = domOffset;
 
 			if ( !viewParent ) {
@@ -548,13 +554,16 @@ export default class DomConverter {
 		// domParent instanceof HTMLElement.
 		else {
 			if ( domOffset === 0 ) {
-				const viewParent = this.getCorrespondingView( domParent );
+				const viewParent = this.mapDomToView( domParent );
 
 				if ( viewParent ) {
 					return new ViewPosition( viewParent, 0 );
 				}
 			} else {
-				const viewBefore = this.getCorrespondingView( domParent.childNodes[ domOffset - 1 ] );
+				const domBefore = domParent.childNodes[ domOffset - 1 ];
+				const viewBefore = this.isText( domBefore ) ?
+					this.findCorrespondingViewText( domBefore ) :
+					this.mapDomToView( domBefore );
 
 				// TODO #663
 				if ( viewBefore && viewBefore.parent ) {
@@ -567,56 +576,22 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding view item. This function use
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingViewElement getCorrespondingViewElement}
-	 * for elements, {@link  module:engine/view/domconverter~DomConverter#getCorrespondingViewText getCorrespondingViewText} for text
-	 * nodes and
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment}
-	 * for document fragments.
-	 *
-	 * Note that for the block or inline {@link module:engine/view/filler filler} this method returns `null`.
-	 *
-	 * @param {Node|DocumentFragment} domNode DOM node or document fragment.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Corresponding view item.
-	 */
-	getCorrespondingView( domNode ) {
-		if ( this.isElement( domNode ) ) {
-			return this.getCorrespondingViewElement( domNode );
-		} else if ( this.isDocumentFragment( domNode ) ) {
-			return this.getCorrespondingViewDocumentFragment( domNode );
-		} else if ( this.isText( domNode ) ) {
-			return this.getCorrespondingViewText( domNode );
-		}
-
-		return null;
-	}
-
-	/**
-	 * Gets corresponding view element. Returns element if an view element was
-	 * {@link module:engine/view/domconverter~DomConverter#bindElements bound} to the given DOM element or `null` otherwise.
+	 * Returns corresponding view {@link module:engine/view/element~Element Element} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment} for provided DOM element or
+	 * document fragment. If there is no view item {@link module:engine/view/domconverter~DomConverter#bindElements bound}
+	 * to the given DOM - `undefined` is returned.
 	 * For all DOM elements rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
 	 *
-	 * @param {HTMLElement} domElement DOM element.
-	 * @returns {module:engine/view/element~Element|null} Corresponding element or `null` if no element was bound.
+	 * @param {DocumentFragment|Element} domElementOrDocumentFragment DOM element or document fragment.
+	 * @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|undefined}
+	 * Corresponding view element, document fragment or `undefined` if no element was bound.
 	 */
-	getCorrespondingViewElement( domElement ) {
-		return this.getParentUIElement( domElement ) || this._domToViewMapping.get( domElement );
+	mapDomToView( domElementOrDocumentFragment ) {
+		return this.getParentUIElement( domElementOrDocumentFragment ) || this._domToViewMapping.get( domElementOrDocumentFragment );
 	}
 
 	/**
-	 * Gets corresponding view document fragment. Returns document fragment if an view element was
-	 * {@link module:engine/view/domconverter~DomConverter#bindDocumentFragments bound} to the given DOM fragment or `null` otherwise.
-	 *
-	 * @param {DocumentFragment} domFragment DOM element.
-	 * @returns {module:engine/view/documentfragment~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 module:engine/view/domconverter~DomConverter#bindElements bound},
+	 * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.
 	 *
 	 * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
@@ -635,7 +610,7 @@ export default class DomConverter {
 	 * @returns {module:engine/view/text~Text|null} Corresponding view text node or `null`, if it was not possible to find a
 	 * corresponding node.
 	 */
-	getCorrespondingViewText( domText ) {
+	findCorrespondingViewText( domText ) {
 		if ( isInlineFiller( domText ) ) {
 			return null;
 		}
@@ -656,7 +631,7 @@ export default class DomConverter {
 				return null;
 			}
 
-			const viewElement = this.getCorrespondingViewElement( previousSibling );
+			const viewElement = this.mapDomToView( previousSibling );
 
 			if ( viewElement ) {
 				const nextSibling = viewElement.nextSibling;
@@ -671,7 +646,7 @@ export default class DomConverter {
 		}
 		// Try to use parent to find the corresponding text node.
 		else {
-			const viewElement = this.getCorrespondingViewElement( domText.parentNode );
+			const viewElement = this.mapDomToView( domText.parentNode );
 
 			if ( viewElement ) {
 				const firstChild = viewElement.getChild( 0 );
@@ -689,52 +664,21 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding DOM item. This function uses
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomElement getCorrespondingDomElement} for
-	 * elements, {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomText getCorrespondingDomText} for text nodes
-	 * and {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}
-	 * for document fragments.
+	 * Returns corresponding DOM item for provided {@link module:engine/view/element~Element Element} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}.
+	 * To find a corresponding text for {@link module:engine/view/text~Text view Text instance}
+	 * use {@link #findCorrespondingDomText}.
 	 *
-	 * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} viewNode View node or document fragment.
-	 * @returns {Node|DocumentFragment|null} Corresponding DOM node or document fragment.
+	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewNode
+	 * View element or document fragment.
+	 * @returns {Node|DocumentFragment|undefined} 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 if ( viewNode instanceof ViewText ) {
-			return this.getCorrespondingDomText( viewNode );
-		}
-
-		return null;
+	mapViewToDom( documentFragmentOrElement ) {
+		return this._viewToDomMapping.get( documentFragmentOrElement );
 	}
 
 	/**
-	 * Gets corresponding DOM element. Returns element if an DOM element was
-	 * {@link module:engine/view/domconverter~DomConverter#bindElements bound} to the given view element or `null` otherwise.
-	 *
-	 * @param {module:engine/view/element~Element} viewElement View element.
-	 * @returns {HTMLElement|null} Corresponding element or `null` if none element was bound.
-	 */
-	getCorrespondingDomElement( viewElement ) {
-		return this._viewToDomMapping.get( viewElement );
-	}
-
-	/**
-	 * Gets corresponding DOM document fragment. Returns document fragment if an DOM element was
-	 * {@link module:engine/view/domconverter~DomConverter#bindDocumentFragments bound} to the given view document fragment or `null`
-	 * otherwise.
-	 *
-	 * @param {module:engine/view/documentfragment~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 module:engine/view/domconverter~DomConverter#bindElements bound},
+	 * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.
 	 *
 	 * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
@@ -748,29 +692,29 @@ export default class DomConverter {
 	 * @param {module:engine/view/text~Text} viewText View text node.
 	 * @returns {Text|null} Corresponding DOM text node or `null`, if it was not possible to find a corresponding node.
 	 */
-	getCorrespondingDomText( viewText ) {
+	findCorrespondingDomText( viewText ) {
 		const previousSibling = viewText.previousSibling;
 
 		// Try to use previous sibling to find the corresponding text node.
-		if ( previousSibling && this.getCorrespondingDom( previousSibling ) ) {
-			return this.getCorrespondingDom( previousSibling ).nextSibling;
+		if ( previousSibling && this.mapViewToDom( previousSibling ) ) {
+			return this.mapViewToDom( previousSibling ).nextSibling;
 		}
 
 		// If this is a first node, try to use parent to find the corresponding text node.
-		if ( !previousSibling && viewText.parent && this.getCorrespondingDom( viewText.parent ) ) {
-			return this.getCorrespondingDom( viewText.parent ).childNodes[ 0 ];
+		if ( !previousSibling && viewText.parent && this.mapViewToDom( viewText.parent ) ) {
+			return this.mapViewToDom( viewText.parent ).childNodes[ 0 ];
 		}
 
 		return null;
 	}
 
 	/**
-	 * Focuses DOM editable that is corresponding to provided {@link module:engine/view/editableelement~EditableElement EditableElement}.
+	 * Focuses DOM editable that is corresponding to provided {@link module:engine/view/editableelement~EditableElement}.
 	 *
 	 * @param {module:engine/view/editableelement~EditableElement} viewEditable
 	 */
 	focus( viewEditable ) {
-		const domEditable = this.getCorrespondingDomElement( viewEditable );
+		const domEditable = this.mapViewToDom( viewEditable );
 
 		if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) {
 			// Save the scrollX and scrollY positions before the focus.
@@ -870,7 +814,7 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Returns parent {@link module:engine/view/uielement~UIElement} for provided DOM node. Returns null if there is no
+	 * Returns parent {@link module:engine/view/uielement~UIElement} for provided DOM node. Returns `null` if there is no
 	 * parent UIElement.
 	 *
 	 * @param {Node} domNode
@@ -898,6 +842,7 @@ export default class DomConverter {
 	 * Takes text data from given {@link module:engine/view/text~Text#data} and processes it so it is correctly displayed in DOM.
 	 *
 	 * Following changes are done:
+	 *
 	 * * multiple spaces are replaced to a chain of spaces and ` `,
 	 * * space at the beginning of the text node is changed to ` ` if it is a first text node in it's container
 	 * element or if previous text node ends by space character,

+ 1 - 1
packages/ckeditor5-engine/src/view/observer/domeventdata.js

@@ -55,7 +55,7 @@ export default class DomEventData {
 	 * @type module:engine/view/element~Element
 	 */
 	get target() {
-		return this.document.domConverter.getCorrespondingViewElement( this.domTarget );
+		return this.document.domConverter.mapDomToView( this.domTarget );
 	}
 
 	/**

+ 5 - 5
packages/ckeditor5-engine/src/view/observer/mutationobserver.js

@@ -147,7 +147,7 @@ export default class MutationObserver extends Observer {
 		// element with changed structure anyway.
 		for ( const mutation of domMutations ) {
 			if ( mutation.type === 'childList' ) {
-				const element = domConverter.getCorrespondingViewElement( mutation.target );
+				const element = domConverter.mapDomToView( mutation.target );
 
 				// Do not collect mutations from UIElements.
 				if ( element && element.is( 'uiElement' ) ) {
@@ -162,7 +162,7 @@ export default class MutationObserver extends Observer {
 
 		// Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
 		for ( const mutation of domMutations ) {
-			const element = domConverter.getCorrespondingViewElement( mutation.target );
+			const element = domConverter.mapDomToView( mutation.target );
 
 			// Do not collect mutations from UIElements.
 			if ( element && element.is( 'uiElement' ) ) {
@@ -170,7 +170,7 @@ export default class MutationObserver extends Observer {
 			}
 
 			if ( mutation.type === 'characterData' ) {
-				const text = domConverter.getCorrespondingViewText( mutation.target );
+				const text = domConverter.findCorrespondingViewText( mutation.target );
 
 				if ( text && !mutatedElements.has( text.parent ) ) {
 					// Use text as a key, for deduplication. If there will be another mutation on the same text element
@@ -186,7 +186,7 @@ export default class MutationObserver extends Observer {
 				// on text, but for the view, where filler text node did not existed, new text node was created, so we
 				// need to fire 'children' mutation instead of 'text'.
 				else if ( !text && startsWithFiller( mutation.target ) ) {
-					mutatedElements.add( domConverter.getCorrespondingViewElement( mutation.target.parentNode ) );
+					mutatedElements.add( domConverter.mapDomToView( mutation.target.parentNode ) );
 				}
 			}
 		}
@@ -203,7 +203,7 @@ export default class MutationObserver extends Observer {
 		}
 
 		for ( const viewElement of mutatedElements ) {
-			const domElement = domConverter.getCorrespondingDomElement( viewElement );
+			const domElement = domConverter.mapViewToDom( viewElement );
 			const viewChildren = viewElement.getChildren();
 			const newViewChildren = domConverter.domChildrenToView( domElement );
 

+ 24 - 17
packages/ckeditor5-engine/src/view/renderer.js

@@ -127,13 +127,13 @@ export default class Renderer {
 	 */
 	markToSync( type, node ) {
 		if ( type === 'text' ) {
-			if ( this.domConverter.getCorrespondingDom( node.parent ) ) {
+			if ( this.domConverter.mapViewToDom( node.parent ) ) {
 				this.markedTexts.add( node );
 			}
 		} else {
 			// If the node has no DOM element it is not rendered yet,
 			// its children/attributes do not need to be marked to be sync.
-			if ( !this.domConverter.getCorrespondingDom( node ) ) {
+			if ( !this.domConverter.mapViewToDom( node ) ) {
 				return;
 			}
 
@@ -165,9 +165,9 @@ export default class Renderer {
 	 * attributes which do not exist in the view element.
 	 *
 	 * For text nodes it updates the text string if it is different. Note that if parent element is marked as an element
-	 * which changed child list, text node update will not be done, because it may not be possible do find a
-	 * {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomText corresponding DOM text}. The change will be handled
-	 * in the parent element.
+	 * which changed child list, text node update will not be done, because it may not be possible to
+	 * {@link module:engine/view/domconverter~DomConverter#findCorrespondingDomText find a corresponding DOM text}.
+	 * The change will be handled in the parent element.
 	 *
 	 * For elements, which child lists have changed, it calculates a {@link module:utils/diff~diff} and adds or removes children which have
 	 * changed.
@@ -190,7 +190,7 @@ export default class Renderer {
 		if ( this._inlineFiller ) {
 			inlineFillerPosition = this._getInlineFillerPosition();
 		}
-		// Othewise, if it's needed, create it at the selection position.
+		// Otherwise, if it's needed, create it at the selection position.
 		else if ( this._needsInlineFillerAtSelection() ) {
 			inlineFillerPosition = this.selection.getFirstPosition();
 
@@ -199,7 +199,7 @@ export default class Renderer {
 		}
 
 		for ( const node of this.markedTexts ) {
-			if ( !this.markedChildren.has( node.parent ) && this.domConverter.getCorrespondingDom( node.parent ) ) {
+			if ( !this.markedChildren.has( node.parent ) && this.domConverter.mapViewToDom( node.parent ) ) {
 				this._updateText( node, { inlineFillerPosition } );
 			}
 		}
@@ -351,7 +351,7 @@ export default class Renderer {
 		const selectionOffset = selectionPosition.offset;
 
 		// If there is no DOM root we do not care about fillers.
-		if ( !this.domConverter.getCorrespondingDomElement( selectionParent.root ) ) {
+		if ( !this.domConverter.mapViewToDom( selectionParent.root ) ) {
 			return false;
 		}
 
@@ -384,7 +384,7 @@ export default class Renderer {
 	 * filler should be rendered.
 	 */
 	_updateText( viewText, options ) {
-		const domText = this.domConverter.getCorrespondingDom( viewText );
+		const domText = this.domConverter.findCorrespondingDomText( viewText );
 		const newDomText = this.domConverter.viewToDom( viewText, domText.ownerDocument );
 
 		const actualText = domText.data;
@@ -408,7 +408,7 @@ export default class Renderer {
 	 * @param {module:engine/view/element~Element} viewElement View element to update.
 	 */
 	_updateAttrs( viewElement ) {
-		const domElement = this.domConverter.getCorrespondingDom( viewElement );
+		const domElement = this.domConverter.mapViewToDom( viewElement );
 		const domAttrKeys = Array.from( domElement.attributes ).map( attr => attr.name );
 		const viewAttrKeys = viewElement.getAttributeKeys();
 
@@ -436,7 +436,7 @@ export default class Renderer {
 	 */
 	_updateChildren( viewElement, options ) {
 		const domConverter = this.domConverter;
-		const domElement = domConverter.getCorrespondingDom( viewElement );
+		const domElement = domConverter.mapViewToDom( viewElement );
 
 		if ( !domElement ) {
 			// If there is no `domElement` it means that it was already removed from DOM.
@@ -445,9 +445,7 @@ export default class Renderer {
 		}
 
 		const domDocument = domElement.ownerDocument;
-
 		const filler = options.inlineFillerPosition;
-
 		const actualDomChildren = domElement.childNodes;
 		const expectedDomChildren = Array.from( domConverter.viewChildrenToDom( viewElement, domDocument, { bind: true } ) );
 
@@ -464,20 +462,29 @@ export default class Renderer {
 		const actions = diff( actualDomChildren, expectedDomChildren, sameNodes );
 
 		let i = 0;
+		const nodesToUnbind = new Set();
 
 		for ( const action of actions ) {
 			if ( action === 'insert' ) {
 				insertAt( domElement, i, expectedDomChildren[ i ] );
 				i++;
 			} else if ( action === 'delete' ) {
-				// Whenever element is removed from DOM, unbind it and all of its children.
-				this.domConverter.unbindDomElement( actualDomChildren[ i ] );
+				nodesToUnbind.add( actualDomChildren[ i ] );
 				remove( actualDomChildren[ i ] );
 			} else { // 'equal'
 				i++;
 			}
 		}
 
+		// Unbind removed nodes. When node does not have a parent it means that it was removed from DOM tree during
+		// comparision with the expected DOM. We don't need to check child nodes, because if child node was reinserted,
+		// it was moved to DOM tree out of the removed node.
+		for ( const node of nodesToUnbind ) {
+			if ( !node.parentNode ) {
+				this.domConverter.unbindDomElement( node );
+			}
+		}
+
 		function sameNodes( actualDomChild, expectedDomChild ) {
 			// Elements.
 			if ( actualDomChild === expectedDomChild ) {
@@ -512,7 +519,7 @@ export default class Renderer {
 			return;
 		}
 
-		const domRoot = this.domConverter.getCorrespondingDomElement( this.selection.editableElement );
+		const domRoot = this.domConverter.mapViewToDom( this.selection.editableElement );
 
 		// Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
 		if ( !this.isFocused || !domRoot ) {
@@ -618,7 +625,7 @@ export default class Renderer {
 
 			if ( domSelection.rangeCount ) {
 				const activeDomElement = doc.activeElement;
-				const viewElement = this.domConverter.getCorrespondingViewElement( activeDomElement );
+				const viewElement = this.domConverter.mapDomToView( activeDomElement );
 
 				if ( activeDomElement && viewElement ) {
 					doc.getSelection().removeAllRanges();

+ 3 - 3
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -66,7 +66,7 @@ describe( 'EditingController', () => {
 			expect( viewRoot ).to.equal( editing.view.getRoot() );
 			expect( domRoot ).to.equal( editing.view.getDomRoot() );
 
-			expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
+			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
 			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 
 			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );
@@ -81,7 +81,7 @@ describe( 'EditingController', () => {
 			expect( viewRoot ).to.equal( editing.view.getRoot( 'header' ) );
 			expect( domRoot ).to.equal( editing.view.getDomRoot( 'header' ) );
 
-			expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
+			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
 			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 
 			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( model.getRoot( 'header' ) );
@@ -100,7 +100,7 @@ describe( 'EditingController', () => {
 
 			expect( domRoot ).to.equal( editing.view.getDomRoot() );
 
-			expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
+			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
 			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 
 			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );

+ 3 - 3
packages/ckeditor5-engine/tests/view/document/document.js

@@ -103,7 +103,7 @@ describe( 'Document', () => {
 			expect( ret ).to.equal( viewRoot );
 
 			expect( domRoot ).to.equal( domDiv );
-			expect( viewDocument.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
+			expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
 
 			expect( viewRoot.name ).to.equal( 'div' );
 			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
@@ -185,7 +185,7 @@ describe( 'Document', () => {
 			expect( count( viewDocument.roots ) ).to.equal( 1 );
 
 			expect( viewDocument.getDomRoot() ).to.equal( domDiv );
-			expect( viewDocument.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
+			expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
 
 			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 		} );
@@ -205,7 +205,7 @@ describe( 'Document', () => {
 			expect( count( viewDocument.roots ) ).to.equal( 2 );
 
 			expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
-			expect( viewDocument.domConverter.getCorrespondingDom( viewH1 ) ).to.equal( domH1 );
+			expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
 
 			expect( viewDocument.getRoot().name ).to.equal( 'div' );
 			expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;

+ 42 - 114
packages/ckeditor5-engine/tests/view/domconverter/binding.js

@@ -30,8 +30,8 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domElement, viewElement );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
-			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
+			expect( converter.mapDomToView( domElement ) ).to.equal( viewElement );
+			expect( converter.mapViewToDom( viewElement ) ).to.equal( domElement );
 		} );
 	} );
 
@@ -42,33 +42,19 @@ describe( 'DomConverter', () => {
 
 			converter.bindDocumentFragments( domFragment, viewFragment );
 
-			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.mapDomToView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.mapViewToDom( viewFragment ) ).to.equal( domFragment );
 		} );
 	} );
 
-	describe( 'getCorrespondingView()', () => {
+	describe( 'mapDomToView()', () => {
 		it( 'should return corresponding view element if element is passed', () => {
 			const domElement = document.createElement( 'p' );
 			const viewElement = new ViewElement( 'p' );
 
 			converter.bindElements( domElement, viewElement );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
-		} );
-
-		it( 'should return corresponding view text if text is passed', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingView( domText ) ).to.equal( viewText );
+			expect( converter.mapDomToView( domElement ) ).to.equal( viewElement );
 		} );
 
 		it( 'should return corresponding view document fragment', () => {
@@ -77,38 +63,16 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domFragment, viewFragment );
 
-			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.mapDomToView( domFragment ) ).to.equal( viewFragment );
 		} );
 
-		it( 'should return null if falsy value was passed', () => {
-			expect( converter.getCorrespondingView( null ) ).to.be.null;
-			expect( converter.getCorrespondingView( undefined ) ).to.be.null;
-		} );
-	} );
-
-	describe( 'getCorrespondingViewElement()', () => {
-		it( 'should return corresponding view element', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingViewElement( domElement ) ).to.equal( viewElement );
-		} );
-	} );
-
-	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 );
+		it( 'should return undefined if falsy value was passed', () => {
+			expect( converter.mapDomToView( null ) ).to.be.undefined;
+			expect( converter.mapDomToView( undefined ) ).to.be.undefined;
 		} );
 	} );
 
-	describe( 'getCorrespondingViewText()', () => {
+	describe( 'findCorrespondingViewText()', () => {
 		it( 'should return corresponding view text based on sibling', () => {
 			const domImg = document.createElement( 'img' );
 			const domText = document.createTextNode( 'foo' );
@@ -121,7 +85,7 @@ describe( 'DomConverter', () => {
 			const viewP = converter.domToView( domP );
 			const viewText = viewP.getChild( 1 );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
+			expect( converter.findCorrespondingViewText( domText ) ).to.equal( viewText );
 		} );
 
 		it( 'should return corresponding view text based on parent', () => {
@@ -133,7 +97,7 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
+			expect( converter.findCorrespondingViewText( domText ) ).to.equal( viewText );
 		} );
 
 		it( 'should return null if sibling is not bound', () => {
@@ -145,7 +109,7 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domText ) ).to.be.null;
 		} );
 
 		it( 'should return null if sibling is not element', () => {
@@ -157,14 +121,14 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingViewText( domTextBar ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domTextBar ) ).to.be.null;
 		} );
 
 		it( 'should return null if parent is not bound', () => {
 			const domText = document.createTextNode( 'foo' );
 			createElement( document, 'p', null, domText );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domText ) ).to.be.null;
 		} );
 
 		it( 'should return null for inline filler', () => {
@@ -175,7 +139,7 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingViewText( domFiller ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domFiller ) ).to.be.null;
 		} );
 
 		it( 'should return null if there is no text node sibling in view', () => {
@@ -192,7 +156,7 @@ describe( 'DomConverter', () => {
 			converter.bindElements( domI, viewI );
 			converter.bindElements( domB, viewB );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domText ) ).to.be.null;
 		} );
 
 		it( 'should return null if there is no child text node in view', () => {
@@ -203,32 +167,18 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+			expect( converter.findCorrespondingViewText( domText ) ).to.be.null;
 		} );
 	} );
 
-	describe( 'getCorrespondingDom()', () => {
+	describe( 'mapViewToDom()', () => {
 		it( 'should return corresponding DOM element if element was passed', () => {
 			const domElement = document.createElement( 'p' );
 			const viewElement = new ViewElement( 'p' );
 
 			converter.bindElements( domElement, viewElement );
 
-			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
-		} );
-
-		it( 'should return corresponding DOM text if text was passed', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingDom( viewText ) ).to.equal( domText );
+			expect( converter.mapViewToDom( viewElement ) ).to.equal( domElement );
 		} );
 
 		it( 'should return corresponding DOM document fragment', () => {
@@ -237,37 +187,15 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domFragment, viewFragment );
 
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.mapViewToDom( viewFragment ) ).to.equal( domFragment );
 		} );
 
-		it( 'should return null if wrong parameter is passed', () => {
-			expect( converter.getCorrespondingDom( null ) ).to.be.null;
-		} );
-	} );
-
-	describe( 'getCorrespondingDomElement()', () => {
-		it( 'should return corresponding DOM element', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingDomElement( viewElement ) ).to.equal( domElement );
-		} );
-	} );
-
-	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 );
+		it( 'should return undefined if wrong parameter is passed', () => {
+			expect( converter.mapViewToDom( null ) ).to.be.undefined;
 		} );
 	} );
 
-	describe( 'getCorrespondingDomText', () => {
+	describe( 'findCorrespondingDomText()', () => {
 		it( 'should return corresponding DOM text based on sibling', () => {
 			const domImg = document.createElement( 'img' );
 			const domText = document.createTextNode( 'foo' );
@@ -283,7 +211,7 @@ describe( 'DomConverter', () => {
 			const viewP = converter.domToView( domP );
 			const viewText = viewP.getChild( 1 );
 
-			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
+			expect( converter.findCorrespondingDomText( viewText ) ).to.equal( domText );
 		} );
 
 		it( 'should return corresponding DOM text based on parent', () => {
@@ -297,7 +225,7 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
+			expect( converter.findCorrespondingDomText( viewText ) ).to.equal( domText );
 		} );
 
 		it( 'should return null if sibling is not bound', () => {
@@ -313,7 +241,7 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domP, viewP );
 
-			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
+			expect( converter.findCorrespondingDomText( viewText ) ).to.be.null;
 		} );
 
 		it( 'should return null if parent is not bound', () => {
@@ -325,14 +253,14 @@ describe( 'DomConverter', () => {
 			const viewP = converter.domToView( domP );
 			const viewText = viewP.getChild( 0 );
 
-			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
+			expect( converter.findCorrespondingDomText( viewText ) ).to.be.null;
 		} );
 
 		it( 'should return null if there is no previous sibling and parent', () => {
 			const domText = document.createTextNode( 'foo' );
 			const viewText = converter.domToView( domText );
 
-			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
+			expect( converter.findCorrespondingDomText( viewText ) ).to.be.null;
 		} );
 	} );
 
@@ -373,13 +301,13 @@ describe( 'DomConverter', () => {
 
 			converter.bindElements( domElement, viewElement );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
-			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
+			expect( converter.mapDomToView( domElement ) ).to.equal( viewElement );
+			expect( converter.mapViewToDom( viewElement ) ).to.equal( domElement );
 
 			converter.unbindDomElement( domElement );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.be.undefined;
-			expect( converter.getCorrespondingDom( viewElement ) ).to.be.undefined;
+			expect( converter.mapDomToView( domElement ) ).to.be.undefined;
+			expect( converter.mapViewToDom( viewElement ) ).to.be.undefined;
 		} );
 
 		it( 'should unbind element\'s child nodes', () => {
@@ -393,26 +321,26 @@ describe( 'DomConverter', () => {
 			converter.bindElements( domElement, viewElement );
 			converter.bindElements( domChild, viewChild );
 
-			expect( converter.getCorrespondingView( domChild ) ).to.equal( viewChild );
-			expect( converter.getCorrespondingDom( viewChild ) ).to.equal( domChild );
+			expect( converter.mapDomToView( domChild ) ).to.equal( viewChild );
+			expect( converter.mapViewToDom( viewChild ) ).to.equal( domChild );
 
 			converter.unbindDomElement( domElement );
 
-			expect( converter.getCorrespondingView( domChild ) ).to.be.undefined;
-			expect( converter.getCorrespondingDom( viewChild ) ).to.be.undefined;
+			expect( converter.mapDomToView( domChild ) ).to.be.undefined;
+			expect( converter.mapViewToDom( viewChild ) ).to.be.undefined;
 		} );
 
 		it( 'should do nothing if there are no elements bind', () => {
 			const domElement = document.createElement( 'p' );
 			const viewElement = new ViewElement( 'p' );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.be.undefined;
-			expect( converter.getCorrespondingDom( viewElement ) ).to.be.undefined;
+			expect( converter.mapDomToView( domElement ) ).to.be.undefined;
+			expect( converter.mapViewToDom( viewElement ) ).to.be.undefined;
 
 			converter.unbindDomElement( domElement );
 
-			expect( converter.getCorrespondingView( domElement ) ).to.be.undefined;
-			expect( converter.getCorrespondingDom( viewElement ) ).to.be.undefined;
+			expect( converter.mapDomToView( domElement ) ).to.be.undefined;
+			expect( converter.mapViewToDom( viewElement ) ).to.be.undefined;
 		} );
 	} );
 } );

+ 10 - 10
packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

@@ -46,8 +46,8 @@ describe( 'DomConverter', () => {
 			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
 			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
 
-			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
-			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domImg );
+			expect( converter.mapViewToDom( viewP ) ).to.not.equal( domP );
+			expect( converter.mapViewToDom( viewP.getChild( 0 ) ) ).to.equal( domImg );
 		} );
 
 		it( 'should create tree of view elements from DOM elements and bind elements', () => {
@@ -67,8 +67,8 @@ describe( 'DomConverter', () => {
 			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
 			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
 
-			expect( converter.getCorrespondingDom( viewP ) ).to.equal( domP );
-			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
+			expect( converter.mapViewToDom( viewP ) ).to.equal( domP );
+			expect( converter.mapViewToDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
 		} );
 
 		it( 'should support unicode', () => {
@@ -82,8 +82,8 @@ describe( 'DomConverter', () => {
 			const viewText = viewP.getChild( 0 );
 			expect( viewText.data ).to.equal( 'நிலைக்கு' );
 
-			expect( converter.getCorrespondingDom( viewP ) ).to.equal( domP );
-			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
+			expect( converter.mapViewToDom( viewP ) ).to.equal( domP );
+			expect( converter.findCorrespondingDomText( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
 		} );
 
 		it( 'should create tree of view elements from DOM element without children', () => {
@@ -104,7 +104,7 @@ describe( 'DomConverter', () => {
 			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
 
 			expect( viewP.childCount ).to.equal( 0 );
-			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
+			expect( converter.mapViewToDom( viewP ) ).to.not.equal( domP );
 		} );
 
 		it( 'should create view document fragment from DOM document fragment', () => {
@@ -122,8 +122,8 @@ describe( 'DomConverter', () => {
 			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 ] );
+			expect( converter.mapViewToDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.mapViewToDom( viewFragment.getChild( 0 ) ) ).to.equal( domFragment.childNodes[ 0 ] );
 		} );
 
 		it( 'should create view document fragment from DOM document fragment without children', () => {
@@ -143,7 +143,7 @@ describe( 'DomConverter', () => {
 			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
 
 			expect( viewFragment.childCount ).to.equal( 0 );
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.not.equal( domFragment );
+			expect( converter.mapViewToDom( viewFragment ) ).to.not.equal( domFragment );
 		} );
 
 		it( 'should return already bind document fragment', () => {

+ 13 - 13
packages/ckeditor5-engine/tests/view/domconverter/uielement.js

@@ -25,7 +25,7 @@ describe( 'DOMConverter UIElement integration', () => {
 		converter = new DomConverter();
 	} );
 
-	describe( 'viewToDom', () => {
+	describe( 'viewToDom()', () => {
 		it( 'should create DOM element from UIElement', () => {
 			const uiElement = new ViewUIElement( 'div' );
 			const domElement = converter.viewToDom( uiElement, document );
@@ -46,13 +46,13 @@ describe( 'DOMConverter UIElement integration', () => {
 			const domElement = converter.viewToDom( myElement, document, { bind: true } );
 			const domSpan = domElement.childNodes[ 0 ];
 
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( myElement );
-			expect( converter.getCorrespondingView( domSpan ) ).to.be.falsy;
-			expect( converter.getCorrespondingView( domSpan.childNodes[ 0 ] ) ).to.be.falsy;
+			expect( converter.mapDomToView( domElement ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domSpan ) ).to.be.falsy;
+			expect( converter.mapDomToView( domSpan.childNodes[ 0 ] ) ).to.be.falsy;
 		} );
 	} );
 
-	describe( 'domToView', () => {
+	describe( 'domToView()', () => {
 		it( 'should return UIElement itself', () => {
 			const uiElement = new MyUIElement( 'div' );
 			const domElement = converter.viewToDom( uiElement, document, { bind: true } );
@@ -74,7 +74,7 @@ describe( 'DOMConverter UIElement integration', () => {
 		} );
 	} );
 
-	describe( 'domPositionToView', () => {
+	describe( 'domPositionToView()', () => {
 		it( 'should convert position inside UIElement to position before it', () => {
 			const uiElement = new MyUIElement( 'h1' );
 			const container = new ViewContainer( 'div', null, [ new ViewContainer( 'div' ), uiElement ] );
@@ -98,32 +98,32 @@ describe( 'DOMConverter UIElement integration', () => {
 		} );
 	} );
 
-	describe( 'getCorrespondingViewElement', () => {
+	describe( 'mapDomToView()', () => {
 		it( 'should return UIElement for DOM elements inside', () => {
 			const myElement = new MyUIElement( 'div' );
 			const domElement = converter.viewToDom( myElement, document, { bind: true } );
 
-			expect( converter.getCorrespondingViewElement( domElement ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domElement ) ).to.equal( myElement );
 
 			const domParagraph = domElement.childNodes[ 0 ];
-			expect( converter.getCorrespondingViewElement( domParagraph ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domParagraph ) ).to.equal( myElement );
 
 			const domSpan = domParagraph.childNodes[ 0 ];
-			expect( converter.getCorrespondingViewElement( domSpan ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domSpan ) ).to.equal( myElement );
 		} );
 	} );
 
-	describe( 'getCorrespondingViewText', () => {
+	describe( 'findCorrespondingViewText()', () => {
 		it( 'should return UIElement for DOM text inside', () => {
 			const myElement = new MyUIElement( 'div' );
 			const domElement = converter.viewToDom( myElement, document, { bind: true } );
 
 			const domText = domElement.querySelector( 'span' ).childNodes[ 0 ];
-			expect( converter.getCorrespondingViewText( domText ) ).to.equal( myElement );
+			expect( converter.findCorrespondingViewText( domText ) ).to.equal( myElement );
 		} );
 	} );
 
-	describe( 'getParentUIElement', () => {
+	describe( 'getParentUIElement()', () => {
 		it( 'should return UIElement for DOM children', () => {
 			const uiElement = new MyUIElement( 'div' );
 			const domElement = converter.viewToDom( uiElement, document, { bind: true } );

+ 10 - 10
packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js

@@ -52,8 +52,8 @@ describe( 'DomConverter', () => {
 			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
 			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
 
-			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
-			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewImg );
+			expect( converter.mapDomToView( domP ) ).not.to.equal( viewP );
+			expect( converter.mapDomToView( domP.childNodes[ 0 ] ) ).to.equal( viewImg );
 		} );
 
 		it( 'should create tree of DOM elements from view elements and bind elements', () => {
@@ -78,8 +78,8 @@ describe( 'DomConverter', () => {
 			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
 			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
 
-			expect( converter.getCorrespondingView( domP ) ).to.equal( viewP );
-			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
+			expect( converter.mapDomToView( domP ) ).to.equal( viewP );
+			expect( converter.mapDomToView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
 		} );
 
 		it( 'should support unicode', () => {
@@ -91,8 +91,8 @@ describe( 'DomConverter', () => {
 			expect( domP.childNodes.length ).to.equal( 1 );
 			expect( domP.childNodes[ 0 ].data ).to.equal( 'நிலைக்கு' );
 
-			expect( converter.getCorrespondingView( domP ) ).to.equal( viewP );
-			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
+			expect( converter.mapDomToView( domP ) ).to.equal( viewP );
+			expect( converter.findCorrespondingViewText( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
 		} );
 
 		it( 'should create tree of DOM elements from view element without children', () => {
@@ -118,7 +118,7 @@ describe( 'DomConverter', () => {
 			expect( domP.attributes.length ).to.equal( 1 );
 
 			expect( domP.childNodes.length ).to.equal( 0 );
-			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
+			expect( converter.mapDomToView( domP ) ).not.to.equal( viewP );
 		} );
 
 		it( 'should create DOM document fragment from view document fragment and bind elements', () => {
@@ -136,8 +136,8 @@ describe( 'DomConverter', () => {
 			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 ) );
+			expect( converter.mapDomToView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.mapDomToView( domFragment.childNodes[ 0 ] ) ).to.equal( viewFragment.getChild( 0 ) );
 		} );
 
 		it( 'should create DOM document fragment from view document without children', () => {
@@ -157,7 +157,7 @@ describe( 'DomConverter', () => {
 			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
 
 			expect( domFragment.childNodes.length ).to.equal( 0 );
-			expect( converter.getCorrespondingView( domFragment ) ).not.to.equal( viewFragment );
+			expect( converter.mapDomToView( domFragment ) ).not.to.equal( viewFragment );
 		} );
 
 		it( 'should return already bind document fragment', () => {

+ 155 - 3
packages/ckeditor5-engine/tests/view/renderer.js

@@ -3,9 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document, window */
+/* globals document, window, NodeFilter */
 
+import ViewDocument from '../../src/view/document';
 import ViewElement from '../../src/view/element';
+import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewText from '../../src/view/text';
 import ViewRange from '../../src/view/range';
 import ViewPosition from '../../src/view/position';
@@ -13,11 +15,13 @@ import Selection from '../../src/view/selection';
 import DomConverter from '../../src/view/domconverter';
 import Renderer from '../../src/view/renderer';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { parse } from '../../src/dev-utils/view';
+import { parse, setData as setViewData, getData as getViewData } from '../../src/dev-utils/view';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '../../src/view/filler';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import log from '@ckeditor/ckeditor5-utils/src/log';
+import { unwrap, insert, remove } from '../../src/view/writer';
+import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
 testUtils.createSinonSandbox();
 
@@ -1260,7 +1264,7 @@ describe( 'Renderer', () => {
 				expect( domRoot.childNodes.length ).to.equal( 2 );
 
 				const container = domRoot.childNodes[ 1 ];
-				expect( domConverter.getCorrespondingViewElement( container ) ).to.be.undefined;
+				expect( domConverter.mapDomToView( container ) ).to.be.undefined;
 				expect( container.childNodes.length ).to.equal( 1 );
 
 				const textNode = container.childNodes[ 0 ];
@@ -1717,6 +1721,154 @@ describe( 'Renderer', () => {
 			} );
 		} );
 	} );
+
+	describe( '#922', () => {
+		let viewDoc, viewRoot, domRoot, converter;
+
+		beforeEach( () => {
+			viewDoc = new ViewDocument();
+			domRoot = document.createElement( 'div' );
+			document.body.appendChild( domRoot );
+			viewRoot = viewDoc.createRoot( domRoot );
+			converter = viewDoc.domConverter;
+		} );
+
+		it( 'should properly render unwrapped attributes #1', () => {
+			setViewData( viewDoc,
+				'<container:p>' +
+					'[<attribute:italic>' +
+						'<attribute:strong>f</attribute:strong>' +
+					'</attribute:italic>]' +
+					'<attribute:strong>oo</attribute:strong>' +
+				'</container:p>'
+			);
+
+			// Render it to DOM to create initial DOM <-> view mappings.
+			viewDoc.render();
+
+			// Unwrap italic attribute element.
+			unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
+			expect( getViewData( viewDoc ) ).to.equal( '<p>[<strong>foo</strong>]</p>' );
+
+			// Re-render changes in view to DOM.
+			viewDoc.render();
+
+			// Check if DOM is rendered correctly.
+			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><strong>foo</strong></p>' );
+			expect( checkMappings() ).to.be.true;
+		} );
+
+		it( 'should properly render unwrapped attributes #2', () => {
+			setViewData( viewDoc,
+				'<container:p>' +
+					'[<attribute:italic>' +
+						'<attribute:strong>foo</attribute:strong>' +
+					'</attribute:italic>]' +
+				'</container:p>' );
+
+			// Render it to DOM to create initial DOM <-> view mappings.
+			viewDoc.render();
+
+			// Unwrap italic attribute element and change text inside.
+			unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
+			viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ).data = 'bar';
+			expect( getViewData( viewDoc ) ).to.equal( '<p>[<strong>bar</strong>]</p>' );
+
+			// Re-render changes in view to DOM.
+			viewDoc.render();
+
+			// Check if DOM is rendered correctly.
+			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><strong>bar</strong></p>' );
+			expect( checkMappings() ).to.be.true;
+		} );
+
+		it( 'should properly render if text is changed and element is inserted into same node #1', () => {
+			setViewData( viewDoc,
+				'<container:p>foo</container:p>'
+			);
+
+			// Render it to DOM to create initial DOM <-> view mappings.
+			viewDoc.render();
+
+			// Change text and insert new element into paragraph.
+			const textNode = viewRoot.getChild( 0 ).getChild( 0 );
+			textNode.data = 'foobar';
+			insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) );
+			expect( getViewData( viewDoc ) ).to.equal( '<p>foobar<img></img></p>' );
+
+			// Re-render changes in view to DOM.
+			viewDoc.render();
+
+			// Check if DOM is rendered correctly.
+			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p>foobar<img></img></p>' );
+			expect( checkMappings() ).to.be.true;
+		} );
+
+		it( 'should properly render if text is changed and element is inserted into same node #2', () => {
+			setViewData( viewDoc,
+				'<container:p>foo</container:p>'
+			);
+
+			// Render it to DOM to create initial DOM <-> view mappings.
+			viewDoc.render();
+
+			// Change text and insert new element into paragraph.
+			const textNode = viewRoot.getChild( 0 ).getChild( 0 );
+			textNode.data = 'foobar';
+			insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) );
+			expect( getViewData( viewDoc ) ).to.equal( '<p><img></img>foobar</p>' );
+
+			// Re-render changes in view to DOM.
+			viewDoc.render();
+
+			// Check if DOM is rendered correctly.
+			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><img></img>foobar</p>' );
+			expect( checkMappings() ).to.be.true;
+		} );
+
+		it( 'should not unbind elements that are removed and reinserted to DOM', () => {
+			setViewData( viewDoc,
+				'<container:p>' +
+					'<attribute:b></attribute:b>' +
+					'<attribute:i></attribute:i>' +
+					'<attribute:span></attribute:span>' +
+				'</container:p>'
+			);
+
+			// Render it to DOM to create initial DOM <-> view mappings.
+			viewDoc.render();
+
+			// Remove first element and reinsert it at the end.
+			const container = viewRoot.getChild( 0 );
+			const firstElement = container.getChild( 0 );
+
+			remove( ViewRange.createOn( firstElement ) );
+			insert( new ViewPosition( container, 2 ), firstElement );
+			expect( getViewData( viewDoc ) ).to.equal( '<p><i></i><span></span><b></b></p>' );
+
+			// Re-render changes in view to DOM.
+			viewDoc.render();
+
+			// Check if DOM is rendered correctly.
+			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><i></i><span></span><b></b></p>' );
+			expect( checkMappings() ).to.be.true;
+		} );
+
+		// Checks if every node in DOM tree is mapped to the view.
+		function checkMappings() {
+			const domWalker = document.createTreeWalker( domRoot, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT );
+
+			while ( domWalker.nextNode() ) {
+				const node = domWalker.currentNode;
+
+				if ( !converter.mapDomToView( node ) && !converter.findCorrespondingViewText( node ) ) {
+					return false;
+				}
+			}
+
+			return true;
+		}
+	} );
 } );
 
 function renderAndExpectNoChanges( renderer, domRoot ) {