8
0
Prechádzať zdrojové kódy

Merge pull request #827 from ckeditor/t/809

Feature: Introduced `is()` method in model and view tree nodes and document fragments. Closes #809.
Piotrek Koszuliński 8 rokov pred
rodič
commit
317cad4a35
52 zmenil súbory, kde vykonal 653 pridanie a 164 odobranie
  1. 1 2
      packages/ckeditor5-engine/src/controller/getselectedcontent.js
  2. 2 3
      packages/ckeditor5-engine/src/controller/insertcontent.js
  3. 4 5
      packages/ckeditor5-engine/src/conversion/mapper.js
  4. 1 2
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  5. 9 12
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  6. 2 4
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  7. 4 5
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  8. 4 5
      packages/ckeditor5-engine/src/dev-utils/model.js
  9. 13 17
      packages/ckeditor5-engine/src/dev-utils/view.js
  10. 3 5
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  11. 12 0
      packages/ckeditor5-engine/src/model/documentfragment.js
  12. 17 0
      packages/ckeditor5-engine/src/model/element.js
  13. 3 4
      packages/ckeditor5-engine/src/model/liveposition.js
  14. 1 1
      packages/ckeditor5-engine/src/model/liveselection.js
  15. 20 0
      packages/ckeditor5-engine/src/model/node.js
  16. 3 5
      packages/ckeditor5-engine/src/model/position.js
  17. 11 0
      packages/ckeditor5-engine/src/model/rootelement.js
  18. 7 0
      packages/ckeditor5-engine/src/model/text.js
  19. 12 0
      packages/ckeditor5-engine/src/model/textproxy.js
  20. 2 2
      packages/ckeditor5-engine/src/model/writer.js
  21. 12 1
      packages/ckeditor5-engine/src/view/attributeelement.js
  22. 11 0
      packages/ckeditor5-engine/src/view/containerelement.js
  23. 12 0
      packages/ckeditor5-engine/src/view/documentfragment.js
  24. 5 6
      packages/ckeditor5-engine/src/view/domconverter.js
  25. 17 0
      packages/ckeditor5-engine/src/view/element.js
  26. 11 0
      packages/ckeditor5-engine/src/view/emptyelement.js
  27. 20 0
      packages/ckeditor5-engine/src/view/node.js
  28. 7 11
      packages/ckeditor5-engine/src/view/position.js
  29. 6 11
      packages/ckeditor5-engine/src/view/range.js
  30. 2 3
      packages/ckeditor5-engine/src/view/renderer.js
  31. 11 0
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  32. 7 0
      packages/ckeditor5-engine/src/view/text.js
  33. 12 0
      packages/ckeditor5-engine/src/view/textproxy.js
  34. 11 0
      packages/ckeditor5-engine/src/view/uielement.js
  35. 30 49
      packages/ckeditor5-engine/src/view/writer.js
  36. 2 2
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  37. 19 0
      packages/ckeditor5-engine/tests/model/documentfragment.js
  38. 23 0
      packages/ckeditor5-engine/tests/model/element.js
  39. 1 1
      packages/ckeditor5-engine/tests/model/position.js
  40. 27 1
      packages/ckeditor5-engine/tests/model/rootelement.js
  41. 19 0
      packages/ckeditor5-engine/tests/model/text.js
  42. 13 0
      packages/ckeditor5-engine/tests/model/textproxy.js
  43. 29 0
      packages/ckeditor5-engine/tests/view/attributeelement.js
  44. 29 0
      packages/ckeditor5-engine/tests/view/containerelement.js
  45. 23 0
      packages/ckeditor5-engine/tests/view/documentfragment.js
  46. 27 0
      packages/ckeditor5-engine/tests/view/element.js
  47. 29 0
      packages/ckeditor5-engine/tests/view/emptyelement.js
  48. 6 6
      packages/ckeditor5-engine/tests/view/position.js
  49. 31 0
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  50. 24 1
      packages/ckeditor5-engine/tests/view/text.js
  51. 17 0
      packages/ckeditor5-engine/tests/view/textproxy.js
  52. 29 0
      packages/ckeditor5-engine/tests/view/uielement.js

+ 1 - 2
packages/ckeditor5-engine/src/controller/getselectedcontent.js

@@ -10,7 +10,6 @@
 import DocumentFragment from '../model/documentfragment';
 import Range from '../model/range';
 import Position from '../model/position';
-import TextProxy from '../model/textproxy';
 import Text from '../model/text';
 import { remove } from '../model/writer';
 
@@ -69,7 +68,7 @@ export default function getSelectedContent( selection ) {
 
 	// Clone the whole contents.
 	for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
-		if ( item instanceof TextProxy ) {
+		if ( item.is( 'textProxy' ) ) {
 			frag.appendChildren( new Text( item.data, item.getAttributes() ) );
 		} else {
 			frag.appendChildren( item.clone( true ) );

+ 2 - 3
packages/ckeditor5-engine/src/controller/insertcontent.js

@@ -9,7 +9,6 @@
 
 import Position from '../model/position';
 import LivePosition from '../model/liveposition';
-import Text from '../model/text';
 import Element from '../model/element';
 import Range from '../model/range';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -218,7 +217,7 @@ class Insertion {
 	 */
 	_handleDisallowedNode( node, context ) {
 		// Try inserting its children (strip the parent).
-		if ( node instanceof Element ) {
+		if ( node.is( 'element' ) ) {
 			this.handleNodes( node.getChildren(), context );
 		}
 		// Try autoparagraphing.
@@ -413,7 +412,7 @@ class Insertion {
 	 * @param {module:engine/model/node~Node} node The node.
 	 */
 	_getNodeSchemaName( node ) {
-		if ( node instanceof Text ) {
+		if ( node.is( 'text' ) ) {
 			return '$text';
 		}
 

+ 4 - 5
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -13,7 +13,6 @@ import ModelRange from '../model/range';
 import ViewPosition from '../view/position';
 import ViewRange from '../view/range';
 import ViewText from '../view/text';
-import ViewUIElement from '../view/uielement';
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -285,7 +284,7 @@ export default class Mapper {
 		// viewBlock == viewParent, so we need to calculate the offset in the parent element.
 
 		// If the position is a text it is simple ("ba|r" -> 2).
-		if ( viewParent instanceof ViewText ) {
+		if ( viewParent.is( 'text' ) ) {
 			return viewOffset;
 		}
 
@@ -340,9 +339,9 @@ export default class Mapper {
 			return callback( viewNode );
 		} else if ( this._viewToModelMapping.has( viewNode ) ) {
 			return 1;
-		} else if ( viewNode instanceof ViewText ) {
+		} else if ( viewNode.is( 'text' ) ) {
 			return viewNode.data.length;
-		} else if ( viewNode instanceof ViewUIElement ) {
+		} else if ( viewNode.is( 'uiElement' ) ) {
 			return 0;
 		} else {
 			let len = 0;
@@ -390,7 +389,7 @@ export default class Mapper {
 		let viewOffset = 0;
 
 		// In the text node it is simple: offset in the model equals offset in the text.
-		if ( viewParent instanceof ViewText ) {
+		if ( viewParent.is( 'text' ) ) {
 			return new ViewPosition( viewParent, expectedOffset );
 		}
 

+ 1 - 2
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -9,7 +9,6 @@
 
 import Consumable from './modelconsumable';
 import Range from '../model/range';
-import TextProxy from '../model/textproxy';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
@@ -463,7 +462,7 @@ export default class ModelConversionDispatcher {
 		}
 
 		if ( type === 'insert' || type === 'remove' || type == 'move' ) {
-			if ( data.item instanceof TextProxy ) {
+			if ( data.item.is( 'textProxy' ) ) {
 				// Example: insert:$text.
 				this.fire( type + ':$text', data, consumable, this.conversionApi );
 			} else {

+ 9 - 12
packages/ckeditor5-engine/src/conversion/viewconsumable.js

@@ -9,9 +9,6 @@
 
 import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import ViewElement from '../view/element';
-import ViewText from '../view/text';
-import ViewDocumentFragment from '../view/documentfragment';
 
 /**
  * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
@@ -83,7 +80,7 @@ export default class ViewConsumable {
 		let elementConsumables;
 
 		// For text nodes and document fragments just mark them as consumable.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+		if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
 			this._consumables.set( element, true );
 
 			return;
@@ -137,7 +134,7 @@ export default class ViewConsumable {
 		}
 
 		// For text nodes and document fragments return stored boolean value.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+		if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
 			return elementConsumables;
 		}
 
@@ -175,7 +172,7 @@ export default class ViewConsumable {
 	 */
 	consume( element, consumables ) {
 		if ( this.test( element, consumables ) ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
 				// For text nodes and document fragments set value to false.
 				this._consumables.set( element, false );
 			} else {
@@ -221,7 +218,7 @@ export default class ViewConsumable {
 		const elementConsumables = this._consumables.get( element );
 
 		if ( elementConsumables !== undefined ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
 				// For text nodes and document fragments - set consumable to true.
 				this._consumables.set( element, true );
 			} else {
@@ -275,11 +272,11 @@ export default class ViewConsumable {
 
 	/**
 	 * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
-	 * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+	 * {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
 	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
 	 *
 	 * @static
-	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
+	 * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment
 	 * from which `ViewConsumable` will be created.
 	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
 	 * to add all consumables. It will be returned instead of a new instance.
@@ -289,18 +286,18 @@ export default class ViewConsumable {
 			instance = new ViewConsumable();
 		}
 
-		if ( from instanceof ViewText ) {
+		if ( from.is( 'text' ) ) {
 			instance.add( from );
 
 			return instance;
 		}
 
 		// Add `from` itself, if it is an element.
-		if ( from instanceof ViewElement ) {
+		if ( from.is( 'element' ) ) {
 			instance.add( from, ViewConsumable.consumablesFromElement( from ) );
 		}
 
-		if ( from instanceof ViewDocumentFragment ) {
+		if ( from.is( 'documentFragment' ) ) {
 			instance.add( from );
 		}
 

+ 2 - 4
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -8,8 +8,6 @@
  */
 
 import ViewConsumable from './viewconsumable';
-import ViewElement from '../view/element';
-import ViewText from '../view/text';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
@@ -150,9 +148,9 @@ export default class ViewConversionDispatcher {
 			output: null
 		} );
 
-		if ( input instanceof ViewElement ) {
+		if ( input.is( 'element' ) ) {
 			this.fire( 'element:' + input.name, data, consumable, this.conversionApi );
-		} else if ( input instanceof ViewText ) {
+		} else if ( input.is( 'text' ) ) {
 			this.fire( 'text', data, consumable, this.conversionApi );
 		} else {
 			this.fire( 'documentFragment', data, consumable, this.conversionApi );

+ 4 - 5
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -38,7 +38,6 @@ import ModelRootElement from '../model/rootelement';
 
 import ViewDocument from '../view/document';
 import ViewElement from '../view/element';
-import ViewText from '../view/text';
 import ViewDocumentFragment from '../view/documentfragment';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -167,7 +166,7 @@ function enableLoggingTools() {
 		for ( let child of this.getChildren() ) {
 			string += '\n';
 
-			if ( child instanceof ModelText ) {
+			if ( child.is( 'text' ) ) {
 				const textAttrs = mapToTags( child._attrs );
 
 				string += '\t'.repeat( level + 1 );
@@ -217,7 +216,7 @@ function enableLoggingTools() {
 		for ( let child of this.getChildren() ) {
 			string += '\n';
 
-			if ( child instanceof ModelText ) {
+			if ( child.is( 'text' ) ) {
 				const textAttrs = mapToTags( child._attrs );
 
 				string += '\t'.repeat( 1 );
@@ -369,7 +368,7 @@ function enableLoggingTools() {
 		string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
 
 		for ( let child of this.getChildren() ) {
-			if ( child instanceof ViewText ) {
+			if ( child.is( 'text' ) ) {
 				string += '\n' + '\t'.repeat( level + 1 ) + child.data;
 			} else {
 				string += '\n' + child.printTree( level + 1 );
@@ -393,7 +392,7 @@ function enableLoggingTools() {
 		let string = 'ViewDocumentFragment: [';
 
 		for ( let child of this.getChildren() ) {
-			if ( child instanceof ViewText ) {
+			if ( child.is( 'text' ) ) {
 				string += '\n' + '\t'.repeat( 1 ) + child.data;
 			} else {
 				string += '\n' + child.printTree( 1 );

+ 4 - 5
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -20,7 +20,6 @@ import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 import ModelElement from '../model/element';
 import ModelText from '../model/text';
-import ModelTextProxy from '../model/textproxy';
 import modelWriter from '../model/writer';
 
 import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
@@ -132,13 +131,13 @@ export function setData( document, data, options = {} ) {
 				// Each range returned from `parse()` method has its root placed in DocumentFragment.
 				// Here we convert each range to have its root re-calculated properly and be placed inside
 				// model document root.
-				if ( range.start.parent instanceof ModelDocumentFragment ) {
+				if ( range.start.parent.is( 'documentFragment' ) ) {
 					start = ModelPosition.createFromParentAndOffset( modelRoot, range.start.offset );
 				} else {
 					start = ModelPosition.createFromParentAndOffset( range.start.parent, range.start.offset );
 				}
 
-				if ( range.end.parent instanceof ModelDocumentFragment ) {
+				if ( range.end.parent.is( 'documentFragment' ) ) {
 					end = ModelPosition.createFromParentAndOffset( modelRoot, range.end.offset );
 				} else {
 					end = ModelPosition.createFromParentAndOffset( range.end.parent, range.end.offset );
@@ -216,7 +215,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 
 	modelToView.on( 'insert:$text', insertText() );
 	modelToView.on( 'addAttribute', wrapItem( ( value, data ) => {
-		if ( data.item instanceof ModelTextProxy ) {
+		if ( data.item.is( 'textProxy' ) ) {
 			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
 		}
 	} ) );
@@ -299,7 +298,7 @@ export function parse( data, schema, options = {} ) {
 	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
 
 	// If root DocumentFragment contains only one element - return that element.
-	if ( model instanceof ModelDocumentFragment && model.childCount == 1 ) {
+	if ( model.is( 'documentFragment' ) && model.childCount == 1 ) {
 		model = model.getChild( 0 );
 	}
 

+ 13 - 17
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -22,7 +22,6 @@ import AttributeElement from '../view/attributeelement';
 import ContainerElement from '../view/containerelement';
 import EmptyElement from '../view/emptyelement';
 import UIElement from '../view/uielement';
-import ViewText from '../view/text';
 
 const ELEMENT_RANGE_START_TOKEN = '[';
 const ELEMENT_RANGE_END_TOKEN = ']';
@@ -327,7 +326,7 @@ export function parse( data, options = {} ) {
 	const ranges = rangeParser.parse( view, options.order );
 
 	// If only one element is returned inside DocumentFragment - return that element.
-	if ( view instanceof ViewDocumentFragment && view.childCount === 1 ) {
+	if ( view.is( 'documentFragment' ) && view.childCount === 1 ) {
 		view = view.getChild( 0 );
 	}
 
@@ -410,7 +409,7 @@ class RangeParser {
 	 * @param {module:engine/view/node~Node} node Staring node.
 	 */
 	_getPositions( node ) {
-		if ( node instanceof ViewDocumentFragment || node instanceof ViewElement ) {
+		if ( node.is( 'documentFragment' ) || node.is( 'element' ) ) {
 			// Copy elements into the array, when nodes will be removed from parent node this array will still have all the
 			// items needed for iteration.
 			const children = [ ...node.getChildren() ];
@@ -420,7 +419,7 @@ class RangeParser {
 			}
 		}
 
-		if ( node instanceof ViewText ) {
+		if ( node.is( 'text' ) ) {
 			const regexp = new RegExp(
 				`[${ TEXT_RANGE_START_TOKEN }${ TEXT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_START_TOKEN }]`,
 				'g'
@@ -619,11 +618,10 @@ class ViewStringify {
 	 * @param {Function} callback
 	 */
 	_walkView( root, callback ) {
-		const isElement = root instanceof ViewElement;
 		const ignore = this.ignoreRoot && this.root === root;
 
-		if ( isElement || root instanceof ViewDocumentFragment ) {
-			if ( isElement && !ignore ) {
+		if ( root.is( 'element' ) || root.is( 'documentFragment' ) ) {
+			if ( root.is( 'element' ) && !ignore ) {
 				callback( this._stringifyElementOpen( root ) );
 			}
 
@@ -636,12 +634,12 @@ class ViewStringify {
 				callback( this._stringifyElementRanges( root, offset ) );
 			}
 
-			if ( isElement && !ignore ) {
+			if ( root.is( 'element' ) && !ignore ) {
 				callback( this._stringifyElementClose( root ) );
 			}
 		}
 
-		if ( root instanceof ViewText ) {
+		if ( root.is( 'text' ) ) {
 			callback( this._stringifyTextRanges( root ) );
 		}
 	}
@@ -804,7 +802,7 @@ class ViewStringify {
 	 * @returns {String}
 	 */
 	_stringifyElementPriority( element ) {
-		if ( this.showPriority && element instanceof AttributeElement ) {
+		if ( this.showPriority && element.is( 'attributeElement' ) ) {
 			return `view-priority="${ element.priority }"`;
 		}
 
@@ -844,19 +842,17 @@ class ViewStringify {
 // @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|
 // module:engine/view/text~Text} Root node of converted elements.
 function _convertViewElements( rootNode ) {
-	const isFragment = rootNode instanceof ViewDocumentFragment;
-
-	if ( rootNode instanceof ViewElement || isFragment ) {
+	if ( rootNode.is( 'element' ) || rootNode.is( 'documentFragment' ) ) {
 		// Convert element or leave document fragment.
-		const convertedElement = isFragment ? new ViewDocumentFragment() : _convertElement( rootNode );
+		const convertedElement = rootNode.is( 'documentFragment' ) ? new ViewDocumentFragment() : _convertElement( rootNode );
 
 		// Convert all child nodes.
 		for ( let child of rootNode.getChildren() ) {
-			if ( convertedElement instanceof EmptyElement ) {
+			if ( convertedElement.is( 'emptyElement' ) ) {
 				throw new Error( `Parse error - cannot parse inside EmptyElement.` );
 			}
 
-			if ( convertedElement instanceof UIElement ) {
+			if ( convertedElement.is( 'uiElement' ) ) {
 				throw new Error( `Parse error - cannot parse inside UIElement.` );
 			}
 
@@ -895,7 +891,7 @@ function _convertElement( viewElement ) {
 	const ElementConstructor = allowedTypes[ info.type ];
 	const newElement = ElementConstructor ? new ElementConstructor( info.name ) : new ViewElement( info.name );
 
-	if ( newElement instanceof AttributeElement ) {
+	if ( newElement.is( 'attributeElement' ) ) {
 		if ( info.priority !== null ) {
 			newElement.priority = info.priority;
 		}

+ 3 - 5
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -14,8 +14,6 @@ import AttributeOperation from '../operation/attributeoperation';
 import RootAttributeOperation from '../operation/rootattributeoperation';
 import Position from '../position';
 import Range from '../range';
-import RootElement from '../rootelement';
-import Element from '../element';
 
 /**
  * To provide specific OT behavior and better collisions solving, methods to change attributes
@@ -166,15 +164,15 @@ function changeItem( batch, doc, key, value, item ) {
 	const previousValue = item.getAttribute( key );
 	let range, operation;
 
-	const delta = item instanceof RootElement ? new RootAttributeDelta() : new AttributeDelta();
+	const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta();
 	batch.addDelta( delta );
 
 	if ( previousValue != value ) {
-		if ( item instanceof RootElement ) {
+		if ( item.is( 'rootElement' ) ) {
 			// If we change attributes of root element, we have to use `RootAttributeOperation`.
 			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
 		} else {
-			if ( item instanceof Element ) {
+			if ( item.is( 'element' ) ) {
 				// If we change the attribute of the element, we do not want to change attributes of its children, so
 				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
 				// it's children, so the range will contain only the opening tag.

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

@@ -96,6 +96,18 @@ export default class DocumentFragment {
 		return null;
 	}
 
+	/**
+	 * Checks whether given model tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/model/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'documentFragment';
+	}
+
 	/**
 	 * Gets the child at the given index. Returns `null` if incorrect index was passed.
 	 *

+ 17 - 0
packages/ckeditor5-engine/src/model/element.js

@@ -80,6 +80,23 @@ export default class Element extends Node {
 		return this.childCount === 0;
 	}
 
+	/**
+	 * Checks whether given model tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/model/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'element' || type == this.name;
+		} else {
+			return type == 'element' && name == this.name;
+		}
+	}
+
 	/**
 	 * Gets the child at the given index.
 	 *

+ 3 - 4
packages/ckeditor5-engine/src/model/liveposition.js

@@ -7,7 +7,6 @@
  * @module engine/model/liveposition
  */
 
-import RootElement from './rootelement';
 import Position from './position';
 import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -40,7 +39,9 @@ export default class LivePosition extends Position {
 	 * See {@link module:engine/model/liveposition~LivePosition#stickiness}.
 	 */
 	constructor( root, path, stickiness ) {
-		if ( !( root instanceof RootElement ) ) {
+		super( root, path );
+
+		if ( !this.root.is( 'rootElement' ) ) {
 			/**
 			 * LivePosition root has to be an instance of RootElement.
 			 *
@@ -49,8 +50,6 @@ export default class LivePosition extends Position {
 			throw new CKEditorError( 'model-liveposition-root-not-rootelement: LivePosition root has to be an instance of RootElement.' );
 		}
 
-		super( root, path );
-
 		/**
 		 * Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node.
 		 * Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if

+ 1 - 1
packages/ckeditor5-engine/src/model/liveselection.js

@@ -651,7 +651,7 @@ export default class LiveSelection extends Selection {
 // Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}. It takes model item, checks whether
 // it is a text node (or text proxy) and if so, returns it's attributes. If not, returns `null`.
 //
-// @param {module:engine/model/item~Item}  node
+// @param {module:engine/model/item~Item|null}  node
 // @returns {Boolean}
 function getAttrsIfCharacter( node ) {
 	if ( node instanceof TextProxy || node instanceof Text ) {

+ 20 - 0
packages/ckeditor5-engine/src/model/node.js

@@ -367,4 +367,24 @@ export default class Node {
 
 		return json;
 	}
+
+	/**
+	 * Checks whether given model tree object is of given type.
+	 *
+	 * This method is useful when processing model tree objects that are of unknown type. For example, a function
+	 * may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node}
+	 * that can be either text node or element. This method can be used to check what kind of object is returned.
+	 *
+	 *		obj.is( 'node' ); // true for any node, false for document fragment
+	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
+	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
+	 *		obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph'
+	 *		obj.is( 'paragraph' ); // shortcut for obj.is( 'element', 'paragraph' )
+	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
+	 *		obj.is( 'textProxy' ); // true for text proxy object
+	 *
+	 * @method #is
+	 * @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type
+	 * @returns {Boolean}
+	 */
 }

+ 3 - 5
packages/ckeditor5-engine/src/model/position.js

@@ -7,8 +7,6 @@
  * @module engine/model/position
  */
 
-import DocumentFragment from './documentfragment';
-import Element from './element';
 import TreeWalker from './treewalker';
 import last from '@ckeditor/ckeditor5-utils/src/lib/lodash/last';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
@@ -45,7 +43,7 @@ export default class Position {
 	 * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
 	 */
 	constructor( root, path ) {
-		if ( !( root instanceof Element ) && !( root instanceof DocumentFragment ) ) {
+		if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
 			/**
 			 * Position root invalid.
 			 *
@@ -293,7 +291,7 @@ export default class Position {
 	 * @returns {Array.<module:engine/model/item~Item>} Array with ancestors.
 	 */
 	getAncestors() {
-		if ( this.parent instanceof DocumentFragment ) {
+		if ( this.parent.is( 'documentFragment' ) ) {
 			return [ this.parent ];
 		} else {
 			return this.parent.getAncestors( { includeNode: true } );
@@ -707,7 +705,7 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position}
 	 */
 	static createFromParentAndOffset( parent, offset ) {
-		if ( !( parent instanceof Element || parent instanceof DocumentFragment ) ) {
+		if ( !parent.is( 'element' ) && !parent.is( 'documentFragment' ) ) {
 			/**
 			 * Position parent have to be a model element or model document fragment.
 			 *

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

@@ -53,6 +53,17 @@ export default class RootElement extends Element {
 		return this._doc;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name ) {
+		if ( !name ) {
+			return type == 'rootElement' || super.is( type );
+		} else {
+			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	/**
 	 * Converts `RootElement` instance to `String` containing it's name.
 	 *

+ 7 - 0
packages/ckeditor5-engine/src/model/text.js

@@ -45,6 +45,13 @@ export default class Text extends Node {
 		return this.data.length;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type ) {
+		return type == 'text';
+	}
+
 	/**
 	 * Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node.
 	 */

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

@@ -171,6 +171,18 @@ export default class TextProxy {
 		return this.textNode.document;
 	}
 
+	/**
+	 * Checks whether given model tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/model/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'textProxy';
+	}
+
 	/**
 	 * Gets path to this text proxy.
 	 *

+ 2 - 2
packages/ckeditor5-engine/src/model/writer.js

@@ -151,7 +151,7 @@ export function setAttribute( range, key, value ) {
 		// Iterator will return `TextProxy` instances but we know that those text proxies will
 		// always represent full text nodes (this is guaranteed thanks to splitting we did before).
 		// So, we can operate on those text proxies' text nodes.
-		let node = item instanceof TextProxy ? item.textNode : item;
+		let node = item.is( 'textProxy' ) ? item.textNode : item;
 
 		if ( value !== null ) {
 			node.setAttribute( key, value );
@@ -239,7 +239,7 @@ function _mergeNodesAtIndex( element, index ) {
 	const nodeAfter = element.getChild( index );
 
 	// Check if both of those nodes are text objects with same attributes.
-	if ( nodeBefore instanceof Text && nodeAfter instanceof Text && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
+	if ( nodeBefore && nodeAfter && nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
 		// Append text of text node after index to the before one.
 		const mergedNode = new Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );
 

+ 12 - 1
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -49,6 +49,17 @@ export default class AttributeElement extends Element {
 		this.getFillerOffset = getFillerOffset;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'attributeElement' || super.is( type );
+		} else {
+			return ( type == 'attributeElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	/**
 	 * Clones provided element with priority.
 	 *
@@ -99,7 +110,7 @@ function getFillerOffset() {
 	let element = this.parent;
 
 	// <p><b></b></p> needs filler -> <p><b><br></b></p>
-	while ( element instanceof AttributeElement ) {
+	while ( element.is( 'attributeElement' ) ) {
 		if ( element.childCount > 1 ) {
 			return null;
 		}

+ 11 - 0
packages/ckeditor5-engine/src/view/containerelement.js

@@ -61,6 +61,17 @@ export default class ContainerElement extends Element {
 		 */
 		this.getFillerOffset = getFillerOffset;
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'containerElement' || super.is( type );
+		} else {
+			return ( type == 'containerElement' && name == this.name ) || super.is( type, name );
+		}
+	}
 }
 
 // Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.

+ 12 - 0
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -83,6 +83,18 @@ export default class DocumentFragment {
 		return null;
 	}
 
+	/**
+	 * Checks whether given view tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'documentFragment';
+	}
+
 	/**
 	 * {@link module:engine/view/documentfragment~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.

+ 5 - 6
packages/ckeditor5-engine/src/view/domconverter.js

@@ -15,7 +15,6 @@ import ViewPosition from './position';
 import ViewRange from './range';
 import ViewSelection from './selection';
 import ViewDocumentFragment from './documentfragment';
-import ViewContainerElement from './containerelement';
 import ViewTreeWalker from './treewalker';
 import { BR_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, isInlineFiller, startsWithFiller, getDataWithoutFiller } from './filler';
 
@@ -160,7 +159,7 @@ export default class DomConverter {
 	 * @returns {Node|DocumentFragment} Converted node or DocumentFragment.
 	 */
 	viewToDom( viewNode, domDocument, options = {} ) {
-		if ( viewNode instanceof ViewText ) {
+		if ( viewNode.is( 'text' ) ) {
 			const textData = this._processDataFromViewText( viewNode );
 
 			return domDocument.createTextNode( textData );
@@ -171,7 +170,7 @@ export default class DomConverter {
 
 			let domElement;
 
-			if ( viewNode instanceof ViewDocumentFragment ) {
+			if ( viewNode.is( 'documentFragment' ) ) {
 				// Create DOM document fragment.
 				domElement = domDocument.createDocumentFragment();
 
@@ -262,7 +261,7 @@ export default class DomConverter {
 	viewPositionToDom( viewPosition ) {
 		const viewParent = viewPosition.parent;
 
-		if ( viewParent instanceof ViewText ) {
+		if ( viewParent.is( 'text' ) ) {
 			const domParent = this.getCorrespondingDomText( viewParent );
 
 			if ( !domParent ) {
@@ -882,11 +881,11 @@ export default class DomConverter {
 		} );
 
 		for ( let value of treeWalker ) {
-			if ( value.item instanceof ViewContainerElement ) {
+			if ( value.item.is( 'containerElement' ) ) {
 				// ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
 				// text node in it's container element.
 				return null;
-			} else if ( value.item instanceof ViewText ) {
+			} else if ( value.item.is( 'text' ) ) {
 				// Found a text node in the same container element.
 				return value.item;
 			}

+ 17 - 0
packages/ckeditor5-engine/src/view/element.js

@@ -136,6 +136,23 @@ export default class Element extends Node {
 		return this._children.length === 0;
 	}
 
+	/**
+	 * Checks whether given view tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'element' || type == this.name;
+		} else {
+			return type == 'element' && name == this.name;
+		}
+	}
+
 	/**
 	 * Clones provided element.
 	 *

+ 11 - 0
packages/ckeditor5-engine/src/view/emptyelement.js

@@ -36,6 +36,17 @@ export default class EmptyElement extends Element {
 		this.getFillerOffset = getFillerOffset;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'emptyElement' || super.is( type );
+		} else {
+			return ( type == 'emptyElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	/**
 	 * Overrides {@link module:engine/view/element~Element#insertChildren} method.
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` to prevent adding any child nodes

+ 20 - 0
packages/ckeditor5-engine/src/view/node.js

@@ -171,6 +171,26 @@ export default class Node {
 	 * @method #isSimilar
 	 * @returns {Boolean} True if nodes are similar.
 	 */
+
+	/**
+	 * Checks whether given view tree object is of given type.
+	 *
+	 * This method is useful when processing view tree objects that are of unknown type. For example, a function
+	 * may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node}
+	 * that can be either text node or element. This method can be used to check what kind of object is returned.
+	 *
+	 *		obj.is( 'node' ); // true for any node, false for document fragment
+	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
+	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
+	 *		obj.is( 'element', 'p' ); // true only for element which name is 'p'
+	 *		obj.is( 'p' ); // shortcut for obj.is( 'element', 'p' )
+	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
+	 *
+	 * @method #is
+	 * @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
+	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
+	 * @returns {Boolean}
+	 */
 }
 
 /**

+ 7 - 11
packages/ckeditor5-engine/src/view/position.js

@@ -7,10 +7,6 @@
  * @module engine/view/position
  */
 
-import Text from './text';
-import TextProxy from './textproxy';
-import DocumentFragment from './documentfragment';
-
 import TreeWalker from './treewalker';
 
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
@@ -52,7 +48,7 @@ export default class Position {
 	 * @type {module:engine/view/node~Node|null}
 	 */
 	get nodeAfter() {
-		if ( this.parent instanceof Text ) {
+		if ( this.parent.is( 'text' ) ) {
 			return null;
 		}
 
@@ -67,7 +63,7 @@ export default class Position {
 	 * @type {module:engine/view/node~Node|null}
 	 */
 	get nodeBefore() {
-		if ( this.parent instanceof Text ) {
+		if ( this.parent.is( 'text' ) ) {
 			return null;
 		}
 
@@ -91,7 +87,7 @@ export default class Position {
 	 * @type {Boolean}
 	 */
 	get isAtEnd() {
-		const endOffset = this.parent instanceof Text ? this.parent.data.length : this.parent.childCount;
+		const endOffset = this.parent.is( 'text' ) ? this.parent.data.length : this.parent.childCount;
 
 		return this.offset === endOffset;
 	}
@@ -172,7 +168,7 @@ export default class Position {
 	 * @returns {Array} Array with ancestors.
 	 */
 	getAncestors() {
-		if ( this.parent instanceof DocumentFragment ) {
+		if ( this.parent.is( 'documentFragment' ) ) {
 			return [ this.parent ];
 		} else {
 			return this.parent.getAncestors( { includeNode: true } );
@@ -307,7 +303,7 @@ export default class Position {
 			let node = itemOrPosition;
 
 			if ( offset == 'end' ) {
-				offset = node instanceof Text ? node.data.length : node.childCount;
+				offset = node.is( 'text' ) ? node.data.length : node.childCount;
 			} else if ( offset == 'before' ) {
 				return this.createBefore( node );
 			} else if ( offset == 'after' ) {
@@ -328,7 +324,7 @@ export default class Position {
 	 */
 	static createAfter( item ) {
 		// TextProxy is not a instance of Node so we need do handle it in specific way.
-		if ( item instanceof TextProxy ) {
+		if ( item.is( 'textProxy' ) ) {
 			return new Position( item.textNode, item.offsetInText + item.data.length );
 		}
 
@@ -353,7 +349,7 @@ export default class Position {
 	 */
 	static createBefore( item ) {
 		// TextProxy is not a instance of Node so we need do handle it in specific way.
-		if ( item instanceof TextProxy ) {
+		if ( item.is( 'textProxy' ) ) {
 			return new Position( item.textNode, item.offsetInText );
 		}
 

+ 6 - 11
packages/ckeditor5-engine/src/view/range.js

@@ -7,14 +7,9 @@
  * @module engine/view/range
  */
 
-import Text from './text';
 import Position from './position';
 import TreeWalker from './treewalker';
 
-import AttributeElement from './attributeelement';
-import ContainerElement from './containerelement';
-import UIElement from './uielement';
-
 /**
  * Tree view range.
  */
@@ -132,11 +127,11 @@ export default class Range {
 		let nodeBeforeEnd = end.nodeBefore;
 
 		// Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
-		if ( nodeAfterStart instanceof Text ) {
+		if ( nodeAfterStart && nodeAfterStart.is( 'text' ) ) {
 			start = new Position( nodeAfterStart, 0 );
 		}
 
-		if ( nodeBeforeEnd instanceof Text ) {
+		if ( nodeBeforeEnd && nodeBeforeEnd.is( 'text' ) ) {
 			end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
 		}
 
@@ -414,11 +409,11 @@ export default class Range {
 
 // Function used by getEnlagred and getShrinked methods.
 function enlargeShrinkStartSkip( value ) {
-	if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
+	if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
 		return true;
 	}
 
-	if ( value.item instanceof ContainerElement && value.type == 'elementStart' ) {
+	if ( value.item.is( 'containerElement' ) && value.type == 'elementStart' ) {
 		return true;
 	}
 
@@ -427,11 +422,11 @@ function enlargeShrinkStartSkip( value ) {
 
 // Function used by getEnlagred and getShrinked methods.
 function enlargeShrinkEndSkip( value ) {
-	if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
+	if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
 		return true;
 	}
 
-	if ( value.item instanceof ContainerElement && value.type == 'elementEnd' ) {
+	if ( value.item.is( 'containerElement' ) && value.type == 'elementEnd' ) {
 		return true;
 	}
 

+ 2 - 3
packages/ckeditor5-engine/src/view/renderer.js

@@ -8,7 +8,6 @@
  */
 
 import ViewText from './text';
-import ViewElement from './element';
 import ViewPosition from './position';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler';
 
@@ -269,7 +268,7 @@ export default class Renderer {
 	_getInlineFillerPosition() {
 		const firstPos = this.selection.getFirstPosition();
 
-		if ( firstPos.parent  instanceof ViewText ) {
+		if ( firstPos.parent.is( 'text' ) ) {
 			return ViewPosition.createBefore( this.selection.getFirstPosition().parent );
 		} else {
 			return firstPos;
@@ -356,7 +355,7 @@ export default class Renderer {
 			return false;
 		}
 
-		if ( !( selectionParent instanceof ViewElement ) ) {
+		if ( !( selectionParent.is( 'element' ) ) ) {
 			return false;
 		}
 

+ 11 - 0
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -38,6 +38,17 @@ export default class RootEditableElement extends EditableElement {
 		this.rootName = 'main';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'rootElement' || super.is( type );
+		} else {
+			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	get rootName() {
 		return this.getCustomProperty( rootNameSymbol );
 	}

+ 7 - 0
packages/ckeditor5-engine/src/view/text.js

@@ -43,6 +43,13 @@ export default class Text extends Node {
 		return new Text( this.data );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type ) {
+		return type == 'text';
+	}
+
 	/**
 	 * The text content.
 	 *

+ 12 - 0
packages/ckeditor5-engine/src/view/textproxy.js

@@ -129,6 +129,18 @@ export default class TextProxy {
 		return this.textNode.document;
 	}
 
+	/**
+	 * Checks whether given view tree object is of given type.
+	 *
+	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'textProxy';
+	}
+
 	/**
 	 * Returns ancestors array of this text proxy.
 	 *

+ 11 - 0
packages/ckeditor5-engine/src/view/uielement.js

@@ -37,6 +37,17 @@ export default class UIElement extends Element {
 		this.getFillerOffset = getFillerOffset;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'uiElement' || super.is( type );
+		} else {
+			return ( type == 'uiElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	/**
 	 * Overrides {@link module:engine/view/element~Element#insertChildren} method.
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` to prevent adding any child nodes

+ 30 - 49
packages/ckeditor5-engine/src/view/writer.js

@@ -12,9 +12,7 @@ import ContainerElement from './containerelement';
 import AttributeElement from './attributeelement';
 import EmptyElement from './emptyelement';
 import UIElement from './uielement';
-import Element from './element';
 import Text from './text';
-import TextProxy from './textproxy';
 import Range from './range';
 import TreeWalker from './treewalker';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -121,7 +119,7 @@ export function breakAttributes( positionOrRange ) {
 export function breakContainer( position ) {
 	const element = position.parent;
 
-	if ( !( element instanceof ContainerElement ) ) {
+	if ( !( element.is( 'containerElement' ) ) ) {
 		/**
 		 * Trying to break an element which is not a container element.
 		 *
@@ -187,12 +185,12 @@ export function mergeAttributes( position ) {
 	const positionParent = position.parent;
 
 	// When inside text node - nothing to merge.
-	if ( positionParent instanceof Text ) {
+	if ( positionParent.is( 'text' ) ) {
 		return position;
 	}
 
 	// When inside empty attribute - remove it.
-	if ( positionParent instanceof AttributeElement && positionParent.childCount === 0 ) {
+	if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
 		const parent = positionParent.parent;
 		const offset = positionParent.index;
 		positionParent.remove();
@@ -208,28 +206,12 @@ export function mergeAttributes( position ) {
 		return position;
 	}
 
-	// When one or both nodes are containers - no attributes to merge.
-	if ( ( nodeBefore instanceof ContainerElement ) || ( nodeAfter instanceof ContainerElement ) ) {
-		return position;
-	}
-
-	// When one or both nodes are EmptyElements - no attributes to merge.
-	if ( ( nodeBefore instanceof EmptyElement ) || ( nodeAfter instanceof EmptyElement ) ) {
-		return position;
-	}
-
-	// When one or both nodes are UIElements - no attributes to merge.
-	if ( ( nodeBefore instanceof UIElement ) || ( nodeAfter instanceof UIElement ) ) {
-		return position;
-	}
-
 	// When position is between two text nodes.
-	if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
+	if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
 		return mergeTextNodes( nodeBefore, nodeAfter );
 	}
-
-	// When selection is between same nodes.
-	else if ( nodeBefore.isSimilar( nodeAfter ) ) {
+	// When selection is between two same attribute elements.
+	else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
 		// Move all children nodes from node placed after selection and remove that node.
 		const count = nodeBefore.childCount;
 		nodeBefore.appendChildren( nodeAfter.getChildren() );
@@ -266,7 +248,7 @@ export function mergeContainers( position ) {
 	const prev = position.nodeBefore;
 	const next = position.nodeAfter;
 
-	if ( !prev || !next || !( prev instanceof ContainerElement ) || !( next instanceof ContainerElement ) ) {
+	if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
 		/**
 		 * Element before and after given position cannot be merged.
 		 *
@@ -302,7 +284,7 @@ export function breakViewRangePerContainer( range ) {
 	let start = range.start;
 
 	for ( let value of walker ) {
-		if ( value.item instanceof ContainerElement ) {
+		if ( value.item.is( 'containerElement' ) ) {
 			if ( !start.isEqual( value.previousPosition ) ) {
 				ranges.push( new Range( start, value.previousPosition ) );
 			}
@@ -439,14 +421,14 @@ export function clear( range, element ) {
 		let rangeToRemove;
 
 		// When current item matches to the given element.
-		if ( item instanceof Element && element.isSimilar( item ) ) {
+		if ( item.is( 'element' ) && element.isSimilar( item ) ) {
 			// Create range on this element.
 			rangeToRemove = Range.createOn( item );
 		// When range starts inside Text or TextProxy element.
-		} else if ( !current.nextPosition.isAfter( range.start ) && ( item instanceof Text || item instanceof TextProxy ) ) {
+		} else if ( !current.nextPosition.isAfter( range.start ) && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) {
 			// We need to check if parent of this text matches to given element.
 			const parentElement = item.getAncestors().find( ( ancestor ) => {
-				return ancestor instanceof Element && element.isSimilar( ancestor );
+				return ancestor.is( 'element' ) && element.isSimilar( ancestor );
 			} );
 
 			// If it is then create range inside this element.
@@ -602,7 +584,7 @@ export function wrapPosition( position, attribute ) {
 	}
 
 	// When position is inside text node - break it and place new position between two text nodes.
-	if ( position.parent instanceof Text ) {
+	if ( position.parent.is( 'text' ) ) {
 		position = breakTextNode( position );
 	}
 
@@ -786,7 +768,7 @@ function _breakAttributes( position, forceSplitText = false ) {
 	const positionParent = position.parent;
 
 	// If position is placed inside EmptyElement - throw an exception as we cannot break inside.
-	if ( position.parent instanceof EmptyElement ) {
+	if ( position.parent.is( 'emptyElement' ) ) {
 		/**
 		 * Cannot break inside EmptyElement instance.
 		 *
@@ -796,7 +778,7 @@ function _breakAttributes( position, forceSplitText = false ) {
 	}
 
 	// If position is placed inside UIElement - throw an exception as we cannot break inside.
-	if ( position.parent instanceof UIElement ) {
+	if ( position.parent.is( 'uiElement' ) ) {
 		/**
 		 * Cannot break inside UIElement instance.
 		 *
@@ -806,7 +788,7 @@ function _breakAttributes( position, forceSplitText = false ) {
 	}
 
 	// There are no attributes to break and text nodes breaking is not forced.
-	if ( !forceSplitText && positionParent instanceof Text && isContainerOrFragment( positionParent.parent ) ) {
+	if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
 		return Position.createFromPosition( position );
 	}
 
@@ -816,7 +798,7 @@ function _breakAttributes( position, forceSplitText = false ) {
 	}
 
 	// Break text and start again in new position.
-	if ( positionParent instanceof Text ) {
+	if ( positionParent.is( 'text' ) ) {
 		return _breakAttributes( breakTextNode( position ), forceSplitText );
 	}
 
@@ -901,7 +883,7 @@ function unwrapChildren( parent, startOffset, endOffset, attribute ) {
 			endOffset += count - 1;
 		} else {
 			// If other nested attribute is found start unwrapping there.
-			if ( child instanceof AttributeElement ) {
+			if ( child.is( 'attributeElement' ) ) {
 				unwrapChildren( child, 0, child.childCount, attribute );
 			}
 
@@ -945,10 +927,10 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 
 	while ( i < endOffset ) {
 		const child = parent.getChild( i );
-		const isText = child instanceof Text;
-		const isAttribute = child instanceof AttributeElement;
-		const isEmpty = child instanceof EmptyElement;
-		const isUI = child instanceof UIElement;
+		const isText = child.is( 'text' );
+		const isAttribute = child.is( 'attributeElement' );
+		const isEmpty = child.is( 'emptyElement' );
+		const isUI = child.is( 'uiElement' );
 
 		// Wrap text, empty elements, ui elements or attributes with higher or equal priority.
 		if ( isText || isEmpty || isUI || ( isAttribute && attribute.priority <= child.priority ) ) {
@@ -961,11 +943,10 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 			parent.insertChildren( i, newAttribute );
 
 			wrapPositions.push(	new Position( parent, i ) );
-		} else {
-			// If other nested attribute is found start wrapping there.
-			if ( child instanceof AttributeElement ) {
-				wrapChildren( child, 0, child.childCount, attribute );
-			}
+		}
+		// If other nested attribute is found start wrapping there.
+		else if ( isAttribute ) {
+			wrapChildren( child, 0, child.childCount, attribute );
 		}
 
 		i++;
@@ -1006,13 +987,13 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 function movePositionToTextNode( position ) {
 	const nodeBefore = position.nodeBefore;
 
-	if ( nodeBefore && nodeBefore instanceof Text ) {
+	if ( nodeBefore && nodeBefore.is( 'text' ) ) {
 		return new Position( nodeBefore, nodeBefore.data.length );
 	}
 
 	const nodeAfter = position.nodeAfter;
 
-	if ( nodeAfter && nodeAfter instanceof Text ) {
+	if ( nodeAfter && nodeAfter.is( 'text' ) ) {
 		return new Position( nodeAfter, 0 );
 	}
 
@@ -1190,7 +1171,7 @@ function unwrapAttributeElement( wrapper, toUnwrap ) {
 // @param {module:engine/view/range~Range} Range
 // @returns {Boolean}
 function rangeSpansOnAllChildren( range ) {
-	return range.start.parent == range.end.parent && range.start.parent instanceof AttributeElement &&
+	return range.start.parent == range.end.parent && range.start.parent.is( 'attributeElement' ) &&
 		range.start.offset === 0 && range.end.offset === range.start.parent.childCount;
 }
 
@@ -1223,7 +1204,7 @@ function validateNodesToInsert( nodes ) {
 			throw new CKEditorError( 'view-writer-insert-invalid-node' );
 		}
 
-		if ( !( node instanceof Text ) ) {
+		if ( !node.is( 'text' ) ) {
 			validateNodesToInsert( node.getChildren() );
 		}
 	}
@@ -1236,7 +1217,7 @@ const validNodesToInsert = [ Text, AttributeElement, ContainerElement, EmptyElem
 // @param {module:engine/view/node~Node} node
 // @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
 function isContainerOrFragment( node ) {
-	return node instanceof ContainerElement || node instanceof DocumentFragment;
+	return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
 }
 
 // Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed

+ 2 - 2
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -181,7 +181,7 @@ describe( 'DataController', () => {
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 		} );
 
-		it( 'should convert content of an element', () => {
+		it( 'should convert content of an element #1', () => {
 			const viewElement = parseView( '<p>foo</p>' );
 			const modelElement = data.toModel( viewElement );
 
@@ -189,7 +189,7 @@ describe( 'DataController', () => {
 			expect( stringify( modelElement ) ).to.equal( '<paragraph>foo</paragraph>' );
 		} );
 
-		it( 'should convert content of an element', () => {
+		it( 'should convert content of an element #2', () => {
 			const viewFragment = parseView( '<p>foo</p><p>bar</p>' );
 			const modelFragment = data.toModel( viewFragment );
 

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

@@ -55,6 +55,25 @@ describe( 'DocumentFragment', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let frag;
+
+		before( () => {
+			frag = new DocumentFragment();
+		} );
+
+		it( 'should return true for documentFragment', () => {
+			expect( frag.is( 'documentFragment' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( frag.is( 'text' ) ).to.be.false;
+			expect( frag.is( 'textProxy' ) ).to.be.false;
+			expect( frag.is( 'element' ) ).to.be.false;
+			expect( frag.is( 'rootElement' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'isEmpty', () => {
 		it( 'should return true if document fragment has no children', () => {
 			let frag = new DocumentFragment();

+ 23 - 0
packages/ckeditor5-engine/tests/model/element.js

@@ -38,6 +38,29 @@ describe( 'Element', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let element;
+
+		before( () => {
+			element = new Element( 'paragraph' );
+		} );
+
+		it( 'should return true for element, element with same name and element name', () => {
+			expect( element.is( 'element' ) ).to.be.true;
+			expect( element.is( 'element', 'paragraph' ) ).to.be.true;
+			expect( element.is( 'paragraph' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( element.is( 'element', 'image' ) ).to.be.false;
+			expect( element.is( 'image' ) ).to.be.false;
+			expect( element.is( 'text' ) ).to.be.false;
+			expect( element.is( 'textProxy' ) ).to.be.false;
+			expect( element.is( 'documentFragment' ) ).to.be.false;
+			expect( element.is( 'rootElement' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'clone', () => {
 		it( 'should return an element with same name, attributes and same instances of children if clone was not deep', () => {
 			let p = new Element( 'p' );

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

@@ -114,7 +114,7 @@ describe( 'position', () => {
 
 			expect( () => {
 				new Position();
-			} ).to.throw( CKEditorError, /model-position-root-invalid/ );
+			} ).to.throw();
 		} );
 	} );
 

+ 27 - 1
packages/ckeditor5-engine/tests/model/rootelement.js

@@ -8,7 +8,7 @@ import Element from '../../src/model/element';
 import RootElement from '../../src/model/rootelement';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 
-describe( 'Element', () => {
+describe( 'RootElement', () => {
 	describe( 'constructor()', () => {
 		it( 'should create root element without attributes', () => {
 			let doc = new Document();
@@ -20,4 +20,30 @@ describe( 'Element', () => {
 			expect( root.childCount ).to.equal( 0 );
 		} );
 	} );
+
+	describe( 'is', () => {
+		let root;
+
+		before( () => {
+			const doc = new Document();
+			root = new RootElement( doc, '$root' );
+		} );
+
+		it( 'should return true for rootElement, element, element with same name and element name', () => {
+			expect( root.is( 'element', '$root' ) ).to.be.true;
+			expect( root.is( 'element' ) ).to.be.true;
+			expect( root.is( '$root' ) ).to.be.true;
+			expect( root.is( 'rootElement', '$root' ) ).to.be.true;
+			expect( root.is( 'rootElement' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( root.is( 'element', '$graveyard' ) ).to.be.false;
+			expect( root.is( 'rootElement', '$graveyard' ) ).to.be.false;
+			expect( root.is( '$graveyard' ) ).to.be.false;
+			expect( root.is( 'text' ) ).to.be.false;
+			expect( root.is( 'textProxy' ) ).to.be.false;
+			expect( root.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
 } );

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

@@ -33,6 +33,25 @@ describe( 'Text', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let text;
+
+		before( () => {
+			text = new Text( 'bar' );
+		} );
+
+		it( 'should return true for text', () => {
+			expect( text.is( 'text' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( text.is( 'textProxy' ) ).to.be.false;
+			expect( text.is( 'element' ) ).to.be.false;
+			expect( text.is( 'rootElement' ) ).to.be.false;
+			expect( text.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'clone', () => {
 		it( 'should return a new Text instance, with data and attributes equal to cloned text node', () => {
 			let text = new Text( 'foo', { bold: true } );

+ 13 - 0
packages/ckeditor5-engine/tests/model/textproxy.js

@@ -100,6 +100,19 @@ describe( 'TextProxy', () => {
 		} ).to.throw( CKEditorError, /model-textproxy-wrong-length/ );
 	} );
 
+	describe( 'is', () => {
+		it( 'should return true for textProxy', () => {
+			expect( textProxy.is( 'textProxy' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( textProxy.is( 'text' ) ).to.be.false;
+			expect( textProxy.is( 'element' ) ).to.be.false;
+			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
+			expect( textProxy.is( 'rootElement' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'getPath', () => {
 		it( 'should return path to the text proxy', () => {
 			expect( textProxy.getPath() ).to.deep.equal( [ 0, 5 ] );

+ 29 - 0
packages/ckeditor5-engine/tests/view/attributeelement.js

@@ -19,6 +19,35 @@ describe( 'AttributeElement', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new AttributeElement( 'span' );
+		} );
+
+		it( 'should return true for attributeElement/element, also with correct name and element name', () => {
+			expect( el.is( 'attributeElement' ) ).to.be.true;
+			expect( el.is( 'attributeElement', 'span' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'span' ) ).to.be.true;
+			expect( el.is( 'span' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'attributeElement', 'p' ) ).to.be.false;
+			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'containerElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'clone', () => {
 		it( 'should clone element with priority', () => {
 			const el = new AttributeElement( 'b' );

+ 29 - 0
packages/ckeditor5-engine/tests/view/containerelement.js

@@ -18,6 +18,35 @@ describe( 'ContainerElement', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new ContainerElement( 'p' );
+		} );
+
+		it( 'should return true for containerElement/element, also with correct name and element name', () => {
+			expect( el.is( 'containerElement' ) ).to.be.true;
+			expect( el.is( 'containerElement', 'p' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'p' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'containerElement', 'span' ) ).to.be.false;
+			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'getFillerOffset', () => {
 		it( 'should return position 0 if element is empty', () => {
 			expect( parse( '<container:p></container:p>' ).getFillerOffset() ).to.equals( 0 );

+ 23 - 0
packages/ckeditor5-engine/tests/view/documentfragment.js

@@ -69,6 +69,29 @@ describe( 'DocumentFragment', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let frag;
+
+		before( () => {
+			frag = new DocumentFragment();
+		} );
+
+		it( 'should return true for documentFragment', () => {
+			expect( frag.is( 'documentFragment' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( frag.is( 'text' ) ).to.be.false;
+			expect( frag.is( 'textProxy' ) ).to.be.false;
+			expect( frag.is( 'element' ) ).to.be.false;
+			expect( frag.is( 'containerElement' ) ).to.be.false;
+			expect( frag.is( 'attributeElement' ) ).to.be.false;
+			expect( frag.is( 'uiElement' ) ).to.be.false;
+			expect( frag.is( 'emptyElement' ) ).to.be.false;
+			expect( frag.is( 'rootElement' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'children manipulation methods', () => {
 		let fragment, el1, el2, el3, el4;
 

+ 27 - 0
packages/ckeditor5-engine/tests/view/element.js

@@ -70,6 +70,33 @@ describe( 'Element', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new Element( 'p' );
+		} );
+
+		it( 'should return true for element, element with correct name and element name', () => {
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'p' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'containerElement' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'isEmpty', () => {
 		it( 'should return true if there are no children in element', () => {
 			const element = new Element( 'p' );

+ 29 - 0
packages/ckeditor5-engine/tests/view/emptyelement.js

@@ -19,6 +19,35 @@ describe( 'EmptyElement', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new EmptyElement( 'p' );
+		} );
+
+		it( 'should return true for emptyElement/element, also with correct name and element name', () => {
+			expect( el.is( 'emptyElement' ) ).to.be.true;
+			expect( el.is( 'emptyElement', 'p' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'p' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'emptyElement', 'span' ) ).to.be.false;
+			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'containerElement' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	it( 'should throw if child elements are passed to constructor', () => {
 		expect( () => {
 			new EmptyElement( 'img', null, [ new Element( 'i' ) ] );

+ 6 - 6
packages/ckeditor5-engine/tests/view/position.js

@@ -403,7 +403,7 @@ describe( 'Position', () => {
 
 	describe( 'compareWith', () => {
 		it( 'should return same if positions are same', () => {
-			const root = new Node();
+			const root = new Element();
 			const position = new Position( root, 0 );
 			const compared = new Position( root, 0 );
 
@@ -411,7 +411,7 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should return before if the position is before compared one', () => {
-			const root = new Node();
+			const root = new Element();
 			const position = new Position( root, 0 );
 			const compared = new Position( root, 1 );
 
@@ -419,7 +419,7 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should return after if the position is after compared one', () => {
-			const root = new Node();
+			const root = new Element();
 			const position = new Position( root, 4 );
 			const compared = new Position( root, 1 );
 
@@ -427,8 +427,8 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should return different if positions are in different roots', () => {
-			const root1 = new Node();
-			const root2 = new Node();
+			const root1 = new Element();
+			const root2 = new Element();
 			const position = new Position( root1, 4 );
 			const compared = new Position( root2, 1 );
 
@@ -436,7 +436,7 @@ describe( 'Position', () => {
 		} );
 
 		it( 'should return correct results if position is in document fragment', () => {
-			const node = new Node( 'name' );
+			const node = new Element( 'name' );
 			const docFrag = new DocumentFragment( [ node ] );
 			const position = new Position( docFrag, 0 );
 			const compared = new Position( docFrag, 1 );

+ 31 - 0
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -38,6 +38,37 @@ describe( 'RootEditableElement', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new RootEditableElement( 'div' );
+		} );
+
+		it( 'should return true for rootElement/containerElement/element, also with correct name and element name', () => {
+			expect( el.is( 'rootElement' ) ).to.be.true;
+			expect( el.is( 'rootElement', 'div' ) ).to.be.true;
+			expect( el.is( 'containerElement' ) ).to.be.true;
+			expect( el.is( 'containerElement', 'div' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'div' ) ).to.be.true;
+			expect( el.is( 'div' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'rootElement', 'p' ) ).to.be.false;
+			expect( el.is( 'containerElement', 'p' ) ).to.be.false;
+			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	it( 'should be cloned properly', () => {
 		const root = new RootEditableElement( 'h1' );
 		root.document = createDocumentMock();

+ 24 - 1
packages/ckeditor5-engine/tests/view/text.js

@@ -6,7 +6,7 @@
 import Node from '../../src/view/node';
 import Text from '../../src/view/text';
 
-describe( 'Element', () => {
+describe( 'Text', () => {
 	describe( 'constructor()', () => {
 		it( 'should create element without attributes', () => {
 			const text = new Text( 'foo' );
@@ -17,6 +17,29 @@ describe( 'Element', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		let text;
+
+		before( () => {
+			text = new Text( 'foo' );
+		} );
+
+		it( 'should return true for text', () => {
+			expect( text.is( 'text' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( text.is( 'textProxy' ) ).to.be.false;
+			expect( text.is( 'element' ) ).to.be.false;
+			expect( text.is( 'containerElement' ) ).to.be.false;
+			expect( text.is( 'attributeElement' ) ).to.be.false;
+			expect( text.is( 'uiElement' ) ).to.be.false;
+			expect( text.is( 'emptyElement' ) ).to.be.false;
+			expect( text.is( 'rootElement' ) ).to.be.false;
+			expect( text.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'clone', () => {
 		it( 'should return new text with same data', () => {
 			const text = new Text( 'foo bar' );

+ 17 - 0
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -61,6 +61,23 @@ describe( 'TextProxy', () => {
 		} );
 	} );
 
+	describe( 'is', () => {
+		it( 'should return true for textProxy', () => {
+			expect( textProxy.is( 'textProxy' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( textProxy.is( 'text' ) ).to.be.false;
+			expect( textProxy.is( 'element' ) ).to.be.false;
+			expect( textProxy.is( 'containerElement' ) ).to.be.false;
+			expect( textProxy.is( 'attributeElement' ) ).to.be.false;
+			expect( textProxy.is( 'uiElement' ) ).to.be.false;
+			expect( textProxy.is( 'emptyElement' ) ).to.be.false;
+			expect( textProxy.is( 'rootElement' ) ).to.be.false;
+			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'getDocument', () => {
 		it( 'should return null if any parent has not set Document', () => {
 			expect( textProxy.document ).to.be.null;

+ 29 - 0
packages/ckeditor5-engine/tests/view/uielement.js

@@ -35,6 +35,35 @@ describe( 'UIElement', () => {
 		} );
 	} );
 
+	describe( 'is()', () => {
+		let el;
+
+		before( () => {
+			el = new UIElement( 'span' );
+		} );
+
+		it( 'should return true for uiElement/element, also with correct name and element name', () => {
+			expect( el.is( 'uiElement' ) ).to.be.true;
+			expect( el.is( 'uiElement', 'span' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'element', 'span' ) ).to.be.true;
+			expect( el.is( 'span' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'uiElement', 'p' ) ).to.be.false;
+			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'containerElement' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'appendChildren()', () => {
 		it( 'should throw when try to append new child element', () => {
 			expect( () => {