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

Merge pull request #447 from ckeditor/t/369

t/369: View.Document as an entry point for the view editing state.
Piotrek Koszuliński 9 лет назад
Родитель
Сommit
0cb1c826f4
22 измененных файлов с 452 добавлено и 177 удалено
  1. 4 4
      packages/ckeditor5-engine/src/model/rootelement.js
  2. 11 12
      packages/ckeditor5-engine/src/view/document.js
  3. 2 2
      packages/ckeditor5-engine/src/view/documentfragment.js
  4. 37 0
      packages/ckeditor5-engine/src/view/editableelement.js
  5. 8 8
      packages/ckeditor5-engine/src/view/element.js
  6. 40 32
      packages/ckeditor5-engine/src/view/node.js
  7. 25 2
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  8. 2 2
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  9. 2 1
      packages/ckeditor5-engine/src/view/observer/observer.js
  10. 3 3
      packages/ckeditor5-engine/src/view/renderer.js
  11. 66 0
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  12. 1 1
      packages/ckeditor5-engine/src/view/text.js
  13. 20 0
      packages/ckeditor5-engine/tests/view/document/integration.js
  14. 3 3
      packages/ckeditor5-engine/tests/view/documentfragment.js
  15. 12 24
      packages/ckeditor5-engine/tests/view/element.js
  16. 2 2
      packages/ckeditor5-engine/tests/view/manual/focusobserver.html
  17. 10 4
      packages/ckeditor5-engine/tests/view/manual/focusobserver.js
  18. 3 0
      packages/ckeditor5-engine/tests/view/manual/focusobserver.md
  19. 33 25
      packages/ckeditor5-engine/tests/view/node.js
  20. 32 0
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  21. 52 52
      packages/ckeditor5-engine/tests/view/renderer.js
  22. 84 0
      packages/ckeditor5-engine/tests/view/rooteditableelement.js

+ 4 - 4
packages/ckeditor5-engine/src/model/rootelement.js

@@ -8,20 +8,20 @@
 import Element from './element.js';
 
 /**
- * Class for nodes that are roots of trees in tree data model.
+ * Class for nodes that are roots of trees in data model.
  *
  * @memberOf engine.model
  * @extends engine.model.Element
  */
 export default class RootElement extends Element {
 	/**
-	 * Creates tree root node.
+	 * Creates root element.
 	 *
 	 * @param {engine.model.Document} doc {@link engine.model.Document} that is an owner of the root.
 	 * @param {String} name Node name.
-	 * @param {String} rootName Root name inside parent {@link engine.model.Document}.
+	 * @param {String} [rootName='main'] Root name inside parent {@link engine.model.Document}.
 	 */
-	constructor( doc, name, rootName ) {
+	constructor( doc, name, rootName = 'main' ) {
 		super( name );
 
 		/**

+ 11 - 12
packages/ckeditor5-engine/src/view/document.js

@@ -9,7 +9,7 @@ import Selection from './selection.js';
 import Renderer from './renderer.js';
 import Writer from './writer.js';
 import DomConverter from './domconverter.js';
-import ContainerElement from './containerelement.js';
+import RootEditableElement from './rooteditableelement.js';
 import { injectQuirksHandling } from './filler.js';
 
 import mix from '../../utils/mix.js';
@@ -152,20 +152,19 @@ export default class Document {
 	 * @param {Element|String} domRoot DOM root element or the tag name of view root element if the DOM element will be
 	 * attached later.
 	 * @param {String} [name='main'] Name of the root.
-	 * @returns {engine.view.ContainerElement} The created view root element.
+	 * @returns {engine.view.RootEditableElement} The created view root element.
 	 */
 	createRoot( domRoot, name = 'main' ) {
 		const rootTag = typeof domRoot == 'string' ? domRoot : domRoot.tagName;
 
-		const viewRoot = new ContainerElement( rootTag );
-		viewRoot.setDocument( this );
+		const viewRoot = new RootEditableElement( this, rootTag, name );
 
 		this.roots.set( name, viewRoot );
 
 		// Mark changed nodes in the renderer.
-		viewRoot.on( 'change', ( evt, type, node ) => {
-			this.renderer.markToSync( type, node );
-		} );
+		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
+		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
+		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
 
 		if ( domRoot instanceof HTMLElement ) {
 			this.attachDomRoot( domRoot, name );
@@ -192,7 +191,7 @@ export default class Document {
 
 		this.domConverter.bindElements( domRoot, viewRoot );
 
-		this.renderer.markToSync( 'CHILDREN', viewRoot );
+		this.renderer.markToSync( 'children', viewRoot );
 
 		for ( let observer of this._observers.values() ) {
 			observer.observe( domRoot, name );
@@ -204,7 +203,7 @@ export default class Document {
 	 * specific "main" root is returned.
 	 *
 	 * @param {String} [name='main'] Name of the root.
-	 * @returns {engine.view.ContainerElement} The view root element with the specified name.
+	 * @returns {engine.view.RootEditableElement} The view root element with the specified name.
 	 */
 	getRoot( name = 'main' ) {
 		return this.roots.get( name );
@@ -258,9 +257,9 @@ mix( Document, EmitterMixin );
  *
  * Possible values:
  *
- * * `CHILDREN` - for child list changes,
- * * `ATTRIBUTES` - for element attributes changes,
- * * `TEXT` - for text nodes changes.
+ * * `children` - for child list changes,
+ * * `attributes` - for element attributes changes,
+ * * `text` - for text nodes changes.
  *
  * @typedef {String} engine.view.ChangeType
  */

+ 2 - 2
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -100,7 +100,7 @@ export default class DocumentFragment {
 	 * @returns {Number} Number of inserted nodes.
 	 */
 	insertChildren( index, nodes ) {
-		this._fireChange( 'CHILDREN', this );
+		this._fireChange( 'children', this );
 		let count = 0;
 
 		if ( !isIterable( nodes ) ) {
@@ -126,7 +126,7 @@ export default class DocumentFragment {
 	 * @returns {Array.<engine.view.Node>} The array of removed nodes.
 	 */
 	removeChildren( index, howMany = 1 ) {
-		this._fireChange( 'CHILDREN', this );
+		this._fireChange( 'children', this );
 
 		for ( let i = index; i < index + howMany; i++ ) {
 			this._children[ i ].parent = null;

+ 37 - 0
packages/ckeditor5-engine/src/view/editableelement.js

@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ContainerElement from './containerelement.js';
+
+import mix from '../../utils/mix.js';
+import ObservableMixin from '../../utils/observablemixin.js';
+
+/**
+ * Editable element which can be a {@link view.engine.RootEditableElement root} or nested editable area in the editor.
+ *
+ * @memberOf engine.view
+ * @extends engine.view.ContainerElement
+ * @mixes utils.ObservaleMixin
+ */
+export default class EditableElement extends ContainerElement {
+	/**
+	 * Creates an editable element.
+	 */
+	constructor( name, attrs, children ) {
+		super( name, attrs, children );
+
+		/**
+		 * Whether the editable is in read-write or read-only mode.
+		 *
+		 * @observable
+		 * @member {Boolean} engine.view.EditableElement#isReadOnly
+		 */
+		this.set( 'isReadOnly', false );
+	}
+}
+
+mix( EditableElement, ObservableMixin );

+ 8 - 8
packages/ckeditor5-engine/src/view/element.js

@@ -257,7 +257,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	setAttribute( key, value ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 
 		if ( key == 'class' ) {
 			parseClasses( this._classes, value );
@@ -278,7 +278,7 @@ export default class Element extends Node {
 	 * @returns {Number} Number of inserted nodes.
 	 */
 	insertChildren( index, nodes ) {
-		this._fireChange( 'CHILDREN', this );
+		this._fireChange( 'children', this );
 		let count = 0;
 
 		if ( !isIterable( nodes ) ) {
@@ -304,7 +304,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	removeAttribute( key ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 
 		// Remove class attribute.
 		if ( key == 'class' ) {
@@ -341,7 +341,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	removeChildren( index, howMany = 1 ) {
-		this._fireChange( 'CHILDREN', this );
+		this._fireChange( 'children', this );
 
 		for ( let i = index; i < index + howMany; i++ ) {
 			this._children[ i ].parent = null;
@@ -413,7 +413,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	addClass( ...className ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 		className.forEach( name => this._classes.add( name ) );
 	}
 
@@ -427,7 +427,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	removeClass( ...className ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 		className.forEach( name => this._classes.delete( name ) );
 	}
 
@@ -473,7 +473,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	setStyle( property, value ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 
 		if ( isPlainObject( property ) ) {
 			const keys = Object.keys( property );
@@ -535,7 +535,7 @@ export default class Element extends Node {
 	 * @fires engine.view.Node#change
 	 */
 	removeStyle( ...property ) {
-		this._fireChange( 'ATTRIBUTES', this );
+		this._fireChange( 'attributes', this );
 		property.forEach( name => this._styles.delete( name ) );
 	}
 }

+ 40 - 32
packages/ckeditor5-engine/src/view/node.js

@@ -29,14 +29,6 @@ export default class Node {
 		 * @member {engine.view.Element|engine.view.DocumentFragment|null} engine.view.Node#parent
 		 */
 		this.parent = null;
-
-		/**
-		 * {@link engine.view.Document} reference.
-		 *
-		 * @protected
-		 * @member {engine.view.Document} engine.view.Node#_document
-		 */
-		this._document = null;
 	}
 
 	/**
@@ -89,16 +81,13 @@ export default class Node {
 	}
 
 	/**
-	 * Gets {@link engine.view.Document} reference. If the node has {@link engine.view.Document}, assign by
-	 * {@link engine.view.Node#setDocument} it will be returned. Otherwise {@link engine.view.Document} of the parents node
-	 * will be returned. If node has no parent, `null` will be returned.
+	 * Gets {@link engine.view.Document} reference, from the {@link engine.view.Node#getRoot root} or
+	 * returns null if the root has no reference to the {@link engine.view.Document}.
 	 *
-	 * @returns {engine.view.Document|null} Tree view of the node, tree view of the parent or null.
+	 * @returns {engine.view.Document|null} View Document of the node or null.
 	 */
 	getDocument() {
-		if ( this._document ) {
-			return this._document;
-		} else if ( this.parent ) {
+		if ( this.parent ) {
 			return this.parent.getDocument();
 		} else {
 			return null;
@@ -106,6 +95,21 @@ export default class Node {
 	}
 
 	/**
+	 * Gets the top parent for the node. If node has no parent it is the root itself.
+	 *
+	 * @returns {engine.view.Node}
+	 */
+	getRoot() {
+		let root = this;
+
+		while ( root.parent ) {
+			root = root.parent;
+		}
+
+		return root;
+	}
+
+	/**
 	 * Returns ancestors array of this node.
 	 *
 	 * @param {Object} options Options object.
@@ -127,16 +131,6 @@ export default class Node {
 	}
 
 	/**
-	 * Sets the {@link engine.view.Document} of the node. Note that not all of nodes need to have {@link engine.view.Document}
-	 * assigned, see {@link engine.view.Node#getDocument}.
-	 *
-	 * @param {engine.view.Document} document Document.
-	 */
-	setDocument( document ) {
-		this._document = document;
-	}
-
-	/**
 	 * Removes node from parent.
 	 */
 	remove() {
@@ -149,7 +143,7 @@ export default class Node {
 	 * @fires engine.view.Node#change
 	 */
 	_fireChange( type, node ) {
-		this.fire( 'change', type, node );
+		this.fire( 'change:' + type, node );
 
 		if ( this.parent ) {
 			this.parent._fireChange( type, node );
@@ -171,15 +165,29 @@ export default class Node {
 	 */
 
 	/**
-	 * Fired when a node changes.
+	 * Fired when list of {@link engine.view.Element elements} children changes.
+	 *
+	 * Change event is bubbled – it is fired on all ancestors.
+	 *
+	 * @event engine.view.Node#change:children
+	 * @param {engine.view.Node} Changed node.
+	 */
+
+	/**
+	 * Fired when list of {@link engine.view.Element elements} attributes changes.
 	 *
-	 * * In case of {@link engine.view.Text text nodes} it will be a change of the text data.
-	 * * In case of {@link engine.view.Element elements} it will be a change of child nodes or attributes.
+	 * Change event is bubbled – it is fired on all ancestors.
+	 *
+	 * @event engine.view.Node#change:attributes
+	 * @param {engine.view.Node} Changed node.
+	 */
+
+	/**
+	 * Fired when {@link engine.view.Text text nodes} data changes.
 	 *
-	 * Change event is bubbling, it is fired on the ancestors chain.
+	 * Change event is bubbled – it is fired on all ancestors.
 	 *
-	 * @event engine.view.Node#change
-	 * @param {engine.view.ChangeType} Type of the change.
+	 * @event engine.view.Node#change:text
 	 * @param {engine.view.Node} Changed node.
 	 */
 }

+ 25 - 2
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -9,6 +9,8 @@ import DomEventObserver from './domeventobserver.js';
 
 /**
  * {@link engine.view.Document#focus Focus} and {@link engine.view.Document#blur blur} events observer.
+ * Focus observer handle also {@link engine.view.RootEditableElement#isFocused isFocused} property of the
+ * {@link engine.view.RootEditableElement root elements}.
  *
  * Note that this observer is attached by the {@link engine.EditingController} and is available by default.
  *
@@ -20,10 +22,31 @@ export default class FocusObserver extends DomEventObserver {
 		super( document );
 
 		this.domEventType = [ 'focus', 'blur' ];
+
+		// Update `isFocus` property of root elements.
+		document.on( 'focus', ( evt, data ) => {
+			const target = data.target;
+
+			for ( let root of document.roots.values() ) {
+				if ( target === root ) {
+					root.isFocused = true;
+				}
+			}
+		} );
+
+		document.on( 'blur', ( evt, data ) => {
+			const target = data.target;
+
+			for ( let root of document.roots.values() ) {
+				if ( target === root ) {
+					root.isFocused = false;
+				}
+			}
+		} );
 	}
 
-	onDomEvent( domEvt ) {
-		this.fire( domEvt.type, domEvt );
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
 	}
 }
 

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

@@ -175,7 +175,7 @@ export default class MutationObserver extends Observer {
 		const viewMutations = [];
 
 		for ( let mutatedText of mutatedTexts.values() ) {
-			this.renderer.markToSync( 'TEXT', mutatedText.node );
+			this.renderer.markToSync( 'text', mutatedText.node );
 			viewMutations.push( mutatedText );
 		}
 
@@ -184,7 +184,7 @@ export default class MutationObserver extends Observer {
 			const viewChildren = viewElement.getChildren();
 			const newViewChildren = domConverter.domChildrenToView( domElement );
 
-			this.renderer.markToSync( 'CHILDREN', viewElement );
+			this.renderer.markToSync( 'children', viewElement );
 			viewMutations.push( {
 				type: 'children',
 				oldChildren: Array.from( viewChildren ),

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

@@ -7,7 +7,8 @@
 
 /**
  * Abstract base observer class. Observers are classes which observe changes on DOM elements, do the preliminary
- * processing and fire events on the {@link engine.view.Document} objects.
+ * processing and fire events on the {@link engine.view.Document} objects. Observers can also add features to the view,
+ * for instance by updating its status or marking elements which need refresh on DOM events.
  *
  * @abstract
  * @memberOf engine.view.observer

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

@@ -113,7 +113,7 @@ export default class Renderer {
 	 * @param {engine.view.Node} node Node to be marked.
 	 */
 	markToSync( type, node ) {
-		if ( type === 'TEXT' ) {
+		if ( type === 'text' ) {
 			if ( this.domConverter.getCorrespondingDom( node.parent ) ) {
 				this.markedTexts.add( node );
 			}
@@ -124,9 +124,9 @@ export default class Renderer {
 				return;
 			}
 
-			if ( type === 'ATTRIBUTES' ) {
+			if ( type === 'attributes' ) {
 				this.markedAttributes.add( node );
-			} else if ( type === 'CHILDREN' ) {
+			} else if ( type === 'children' ) {
 				this.markedChildren.add( node );
 			} else {
 				/**

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

@@ -0,0 +1,66 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EditableElement from './editableelement.js';
+
+/**
+ * Class representing a single root in the data view. A root can be either {@link #isReadOnly editable or read-only}, but
+ * in both cases it is called "an editable". Roots can contain other {@link engine.view.EditableElement editable elements}
+ * making them "nested editables".
+ *
+ * @memberOf engine.view
+ * @extends engine.view.EditableElement
+ */
+export default class RootEditableElement extends EditableElement {
+	/**
+	 * Creates root editable element.
+	 *
+	 * @param {engine.view.Document} document {@link engine.view.Document} that is an owner of the root.
+	 * @param {String} name Node name.
+	 * @param {String} [rootName='main'] Root name inside parent {@link engine.view.Document}.
+	 */
+	constructor( document, name, rootName = 'main' ) {
+		super( name );
+
+		/**
+		 * {@link engine.view.Document} that is an owner of this root.
+		 *
+		 * @private
+		 * @member {engine.view.Document} engine.view.RootEditableElement#_document
+		 */
+		this._document = document;
+
+		/**
+		 * Name of this root inside {@link engine.view.Document} that is an owner of this root.
+		 *
+		 * @readonly
+		 * @member {String} engine.view.RootEditableElement#rootName
+		 */
+		this.rootName = rootName;
+
+		/**
+		 * Whether the editable is focused.
+		 *
+		 * This property is updated by the {@link engine.view.obsever.FocusObserver}.
+		 * If the {@link engine.view.obsever.FocusObserver} is disabled this property will not change.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} engine.view.RootEditableElement#isFocused
+		 */
+		this.set( 'isFocused', false );
+	}
+
+	/**
+	 * Gets the {@link engine.view.Document} reference.
+	 *
+	 * @returns {engine.view.Document|null} View Document of the node or `null`.
+	 */
+	getDocument() {
+		return this._document;
+	}
+}

+ 1 - 1
packages/ckeditor5-engine/src/view/text.js

@@ -52,7 +52,7 @@ export default class Text extends Node {
 	}
 
 	set data( data ) {
-		this._fireChange( 'TEXT', this );
+		this._fireChange( 'text', this );
 
 		this._data = data;
 	}

+ 20 - 0
packages/ckeditor5-engine/tests/view/document/integration.js

@@ -40,4 +40,24 @@ describe( 'Document integration', () => {
 		expect( domDiv.childNodes.length ).to.equal( 1 );
 		expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
 	} );
+
+	it( 'should render attribute changes', () => {
+		const domRoot = document.createElement( 'div' );
+
+		const viewDocument = new Document();
+		const viewRoot = viewDocument.createRoot( domRoot );
+
+		const viewP = new ViewElement( 'p', { class: 'foo' } );
+		viewRoot.appendChildren( viewP );
+		viewDocument.render();
+
+		expect( domRoot.childNodes.length ).to.equal( 1 );
+		expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
+
+		viewP.setAttribute( 'class', 'bar' );
+		viewDocument.render();
+
+		expect( domRoot.childNodes.length ).to.equal( 1 );
+		expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
+	} );
 } );

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

@@ -91,7 +91,7 @@ describe( 'DocumentFragment', () => {
 
 			it( 'should fire change event when inserting', ( done ) => {
 				fragment.once( 'change', ( event, type ) => {
-					expect( type ).to.equal( 'CHILDREN' );
+					expect( type ).to.equal( 'children' );
 					done();
 				} );
 
@@ -100,7 +100,7 @@ describe( 'DocumentFragment', () => {
 
 			it( 'should fire change event when appending', ( done ) => {
 				fragment.once( 'change', ( event, type ) => {
-					expect( type ).to.equal( 'CHILDREN' );
+					expect( type ).to.equal( 'children' );
 					done();
 				} );
 
@@ -177,7 +177,7 @@ describe( 'DocumentFragment', () => {
 				fragment.appendChildren( el1 );
 
 				fragment.once( 'change', ( event, type ) => {
-					expect( type ).to.equal( 'CHILDREN' );
+					expect( type ).to.equal( 'children' );
 					done();
 				} );
 

+ 12 - 24
packages/ckeditor5-engine/tests/view/element.js

@@ -340,11 +340,9 @@ describe( 'Element', () => {
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' );
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+			it( 'should fire change event with attributes type', ( done ) => {
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 
@@ -486,12 +484,10 @@ describe( 'Element', () => {
 				expect( count( el.getAttributeKeys() ) ).to.equal( 0 );
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+			it( 'should fire change event with attributes type', ( done ) => {
 				el.setAttribute( 'foo', 'bar' );
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 
@@ -541,11 +537,9 @@ describe( 'Element', () => {
 				expect( el._classes.has( 'one' ) ).to.be.true;
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+			it( 'should fire change event with attributes type', ( done ) => {
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 
@@ -572,12 +566,10 @@ describe( 'Element', () => {
 				expect( el._classes.has( 'three' ) ).to.be.true;
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+			it( 'should fire change event with attributes type', ( done ) => {
 				el.addClass( 'one' );
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 
@@ -644,11 +636,9 @@ describe( 'Element', () => {
 				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+			it( 'should fire change event with attributes type', ( done ) => {
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 
@@ -725,12 +715,10 @@ describe( 'Element', () => {
 				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
 			} );
 
-			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+			it( 'should fire change event with attributes type', ( done ) => {
 				el.setStyle( 'color', 'red' );
-				el.once( 'change', ( eventInfo, type ) => {
-					expect( eventInfo.name ).to.equal( 'change' );
+				el.once( 'change:attributes', ( eventInfo ) => {
 					expect( eventInfo.source ).to.equal( el );
-					expect( type ).to.equal( 'ATTRIBUTES' );
 					done();
 				} );
 

+ 2 - 2
packages/ckeditor5-engine/tests/view/manual/focusobserver.html

@@ -1,6 +1,6 @@
-<div contenteditable="true" id="editable1" style="border: solid 1px blue">
+<div contenteditable="true" id="editable1" style="border: solid 1px black; background-color: red">
 	<p>First editable.</p>
 </div>
-<div contenteditable="true" id="editable2" style="border: solid 1px blue">
+<div contenteditable="true" id="editable2" style="border: solid 1px black; background-color: red">
 	<p>Second editable.</p>
 </div>

+ 10 - 4
packages/ckeditor5-engine/tests/view/manual/focusobserver.js

@@ -12,10 +12,16 @@ import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 
 const viewDocument = new Document();
 
-viewDocument.on( 'focus', ( evt, data ) => console.log( 'focus', data.domTarget ) );
-viewDocument.on( 'blur', ( evt, data ) => console.log( 'blur', data.domTarget ) );
+viewDocument.on( 'focus', ( evt, data ) => console.log( 'event:focus', data.domTarget ) );
+viewDocument.on( 'blur', ( evt, data ) => console.log( 'event:blur', data.domTarget ) );
 
 viewDocument.addObserver( FocusObserver );
 
-viewDocument.createRoot( document.getElementById( 'editable1' ), 'editable1' );
-viewDocument.createRoot( document.getElementById( 'editable2' ), 'editable2' );
+const domEditable1 = document.getElementById( 'editable1' );
+const domEditable2 = document.getElementById( 'editable2' );
+
+const editable1 = viewDocument.createRoot( domEditable1, 'editable1' );
+const editable2 = viewDocument.createRoot( domEditable2, 'editable2' );
+
+editable1.on( 'change:isFocused', () => domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red' );
+editable2.on( 'change:isFocused', () => domEditable2.style.backgroundColor = editable2.isFocused ? 'green' : 'red' );

+ 3 - 0
packages/ckeditor5-engine/tests/view/manual/focusobserver.md

@@ -1,6 +1,9 @@
 @bender-ui: collapsed
 @bender-tags: view
 
+Focused editable should be green.
+Blurred editable should be red.
+
 * Open console.
 * Focus the first editable.
   * Focus in the 1st editable should be logged.

+ 33 - 25
packages/ckeditor5-engine/tests/view/node.js

@@ -9,6 +9,7 @@
 
 import Element from '/ckeditor5/engine/view/element.js';
 import Text from '/ckeditor5/engine/view/text.js';
+import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 describe( 'Node', () => {
@@ -133,29 +134,36 @@ describe( 'Node', () => {
 			expect( charA.getDocument() ).to.be.null;
 		} );
 
-		it( 'should return view attached to the element', () => {
-			const tvMock = {};
-			const element = new Element( 'p' );
-
-			element.setDocument( tvMock );
-
-			expect( element.getDocument() ).to.equal( tvMock );
-		} );
-
 		it( 'should return Document attached to the parent element', () => {
 			const docMock = {};
-			const parent = new Element( 'div' );
+			const parent = new RootEditableElement( docMock, 'div' );
 			const child = new Element( 'p' );
 
 			child.parent = parent;
 
-			parent.setDocument( docMock );
-
 			expect( parent.getDocument() ).to.equal( docMock );
 			expect( child.getDocument() ).to.equal( docMock );
 		} );
 	} );
 
+	describe( 'getRoot', () => {
+		it( 'should return this element if it has no parent', () => {
+			const child = new Element( 'p' );
+
+			expect( child.getRoot() ).to.equal( child );
+		} );
+
+		it( 'should return root element', () => {
+			const parent = new RootEditableElement( {}, 'div' );
+			const child = new Element( 'p' );
+
+			child.parent = parent;
+
+			expect( parent.getRoot() ).to.equal( parent );
+			expect( child.getRoot() ).to.equal( parent );
+		} );
+	} );
+
 	describe( 'remove', () => {
 		it( 'should remove node from its parent', () => {
 			const char = new Text( 'a' );
@@ -193,9 +201,9 @@ describe( 'Node', () => {
 			root = new Element( 'p', { renderer: { markToSync: rootChangeSpy } } );
 			root.appendChildren( [ text, img ] );
 
-			root.on( 'change', ( evt, type, node ) => {
-				rootChangeSpy( type, node );
-			} );
+			root.on( 'change:children', ( evt, node ) => rootChangeSpy( 'children', node ) );
+			root.on( 'change:attributes', ( evt, node ) => rootChangeSpy( 'attributes', node ) );
+			root.on( 'change:text', ( evt, node ) => rootChangeSpy( 'text', node ) );
 
 			rootChangeSpy.reset();
 		} );
@@ -203,21 +211,21 @@ describe( 'Node', () => {
 		it( 'should be fired on the node', () => {
 			const imgChangeSpy = sinon.spy();
 
-			img.on( 'change', ( evt, type, node ) => {
-				imgChangeSpy( type, node );
+			img.on( 'change:attributes', ( evt, node ) => {
+				imgChangeSpy( 'attributes', node );
 			} );
 
 			img.setAttribute( 'width', 100 );
 
 			sinon.assert.calledOnce( imgChangeSpy );
-			sinon.assert.calledWith( imgChangeSpy, 'ATTRIBUTES', img );
+			sinon.assert.calledWith( imgChangeSpy, 'attributes', img );
 		} );
 
 		it( 'should be fired on the parent', () => {
 			img.setAttribute( 'width', 100 );
 
 			sinon.assert.calledOnce( rootChangeSpy );
-			sinon.assert.calledWith( rootChangeSpy, 'ATTRIBUTES', img );
+			sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 		} );
 
 		describe( 'setAttr', () => {
@@ -225,7 +233,7 @@ describe( 'Node', () => {
 				img.setAttribute( 'width', 100 );
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'ATTRIBUTES', img );
+				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 			} );
 		} );
 
@@ -234,7 +242,7 @@ describe( 'Node', () => {
 				img.removeAttribute( 'src' );
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'ATTRIBUTES', img );
+				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 			} );
 		} );
 
@@ -243,7 +251,7 @@ describe( 'Node', () => {
 				root.insertChildren( 1, new Element( 'img' ) );
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'CHILDREN', root );
+				sinon.assert.calledWith( rootChangeSpy, 'children', root );
 			} );
 		} );
 
@@ -252,7 +260,7 @@ describe( 'Node', () => {
 				root.appendChildren( new Element( 'img' ) );
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'CHILDREN', root );
+				sinon.assert.calledWith( rootChangeSpy, 'children', root );
 			} );
 		} );
 
@@ -261,7 +269,7 @@ describe( 'Node', () => {
 				root.removeChildren( 1, 1 );
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'CHILDREN', root );
+				sinon.assert.calledWith( rootChangeSpy, 'children', root );
 			} );
 		} );
 
@@ -270,7 +278,7 @@ describe( 'Node', () => {
 				text.data = 'bar';
 
 				sinon.assert.calledOnce( rootChangeSpy );
-				sinon.assert.calledWith( rootChangeSpy, 'TEXT', text );
+				sinon.assert.calledWith( rootChangeSpy, 'text', text );
 			} );
 		} );
 	} );

+ 32 - 0
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -49,4 +49,36 @@ describe( 'FocusObserver', () => {
 			expect( data.domTarget ).to.equal( document.body );
 		} );
 	} );
+
+	describe( 'handle isFocused property of root elements', () => {
+		let domMain, domHeader, viewMain, viewHeader;
+
+		beforeEach( () => {
+			domMain = document.createElement( 'div' );
+			domHeader = document.createElement( 'h1' );
+
+			viewMain = viewDocument.createRoot( domMain );
+			viewHeader = viewDocument.createRoot( domHeader, 'header' );
+		} );
+
+		it( 'should change isFocused on focus event', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			expect( viewMain.isFocused ).to.be.true;
+			expect( viewHeader.isFocused ).to.be.false;
+		} );
+
+		it( 'should change isFocused on blur event', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			expect( viewMain.isFocused ).to.be.true;
+			expect( viewHeader.isFocused ).to.be.false;
+
+			observer.onDomEvent( { type: 'blur', target: domMain } );
+			observer.onDomEvent( { type: 'focus', target: domHeader } );
+
+			expect( viewMain.isFocused ).to.be.false;
+			expect( viewHeader.isFocused ).to.be.true;
+		} );
+	} );
 } );

+ 52 - 52
packages/ckeditor5-engine/tests/view/renderer.js

@@ -44,7 +44,7 @@ describe( 'Renderer', () => {
 		it( 'should mark attributes which need update', () => {
 			viewRoot.setAttribute( 'class', 'foo' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewRoot );
+			renderer.markToSync( 'attributes', viewRoot );
 
 			expect( renderer.markedAttributes.has( viewRoot ) ).to.be.true;
 		} );
@@ -52,7 +52,7 @@ describe( 'Renderer', () => {
 		it( 'should mark children which need update', () => {
 			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 
 			expect( renderer.markedChildren.has( viewRoot ) ).to.be.true;
 		} );
@@ -63,7 +63,7 @@ describe( 'Renderer', () => {
 
 			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 
 			expect( renderer.markedTexts.has( viewRoot ) ).to.be.false;
 		} );
@@ -73,7 +73,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewText );
 			viewText.data = 'bar';
 
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'text', viewText );
 
 			expect( renderer.markedTexts.has( viewText ) ).to.be.true;
 		} );
@@ -86,7 +86,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewText );
 			viewText.data = 'bar';
 
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'text', viewText );
 
 			expect( renderer.markedTexts.has( viewText ) ).to.be.false;
 		} );
@@ -118,7 +118,7 @@ describe( 'Renderer', () => {
 		it( 'should update attributes', () => {
 			viewRoot.setAttribute( 'class', 'foo' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewRoot );
+			renderer.markToSync( 'attributes', viewRoot );
 			renderer.render();
 
 			expect( domRoot.getAttribute( 'class' ) ).to.equal( 'foo' );
@@ -131,7 +131,7 @@ describe( 'Renderer', () => {
 			domRoot.setAttribute( 'id', 'bar' );
 			domRoot.setAttribute( 'class', 'bar' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewRoot );
+			renderer.markToSync( 'attributes', viewRoot );
 			renderer.render();
 
 			expect( domRoot.getAttribute( 'class' ) ).to.equal( 'foo' );
@@ -143,7 +143,7 @@ describe( 'Renderer', () => {
 		it( 'should add children', () => {
 			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -155,7 +155,7 @@ describe( 'Renderer', () => {
 		it( 'should remove children', () => {
 			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -163,7 +163,7 @@ describe( 'Renderer', () => {
 
 			viewRoot.removeChildren( 0, 1 );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 0 );
@@ -175,7 +175,7 @@ describe( 'Renderer', () => {
 			const viewText = new ViewText( 'foo' );
 			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -183,7 +183,7 @@ describe( 'Renderer', () => {
 
 			viewText.data = 'bar';
 
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'text', viewText );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -197,8 +197,8 @@ describe( 'Renderer', () => {
 			const viewText = new ViewText( 'foo' );
 			viewRoot.appendChildren( [ viewImg, viewText ] );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'children', viewRoot );
+			renderer.markToSync( 'text', viewText );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -210,13 +210,13 @@ describe( 'Renderer', () => {
 			const viewText = new ViewText( 'foo' );
 			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			// This should not be changed during the render.
 			const domText = domRoot.childNodes[ 0 ];
 
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'text', viewText );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -227,13 +227,13 @@ describe( 'Renderer', () => {
 			const viewText = new ViewText( 'foo' );
 			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			// This should not be changed during the render.
 			const domText = domRoot.childNodes[ 0 ];
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -250,7 +250,7 @@ describe( 'Renderer', () => {
 
 			domConverter.bindElements( domImg, viewImg );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -261,14 +261,14 @@ describe( 'Renderer', () => {
 			const viewImg = new ViewElement( 'img' );
 			viewRoot.appendChildren( viewImg );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const viewP = new ViewElement( 'p' );
 			viewRoot.removeChildren( 0, 1 );
 			viewRoot.appendChildren( viewP );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -285,7 +285,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -306,8 +306,8 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 2: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewRoot );
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewRoot );
+			renderer.markToSync( 'children', viewP );
 
 			renderAndExpectNoChanges( renderer, domRoot );
 
@@ -329,8 +329,8 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 4: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewRoot );
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewRoot );
+			renderer.markToSync( 'children', viewP );
 
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
@@ -345,7 +345,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -362,7 +362,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 2: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>{}foo</b></p>
@@ -383,7 +383,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 4: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
@@ -397,7 +397,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -414,7 +414,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 2: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>foo{}</b></p>
@@ -435,7 +435,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 
 			// Step 4: No mutation on second render
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
@@ -448,7 +448,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -476,7 +476,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -504,7 +504,7 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
@@ -516,7 +516,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -535,7 +535,7 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderer.render();
 
 			expect( domP.childNodes.length ).to.equal( 1 );
@@ -555,7 +555,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -583,7 +583,7 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
 
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
@@ -596,7 +596,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -630,11 +630,11 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
-		it( 'should handle typing in empty attribute as a CHILDREN change, render if needed', () => {
+		it( 'should handle typing in empty attribute as a children change, render if needed', () => {
 			const domSelection = document.getSelection();
 
 			const { view: viewP, selection: newSelection } = parse(
@@ -643,7 +643,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -669,7 +669,7 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
-			renderer.markToSync( 'CHILDREN', viewB );
+			renderer.markToSync( 'children', viewB );
 			renderer.render();
 
 			expect( domB.childNodes.length ).to.equal( 1 );
@@ -681,7 +681,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
 		} );
 
-		it( 'should handle typing in empty attribute as a TEXT change, render if needed', () => {
+		it( 'should handle typing in empty attribute as a text change, render if needed', () => {
 			const domSelection = document.getSelection();
 
 			const { view: viewP, selection: newSelection } = parse(
@@ -690,7 +690,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -716,7 +716,7 @@ describe( 'Renderer', () => {
 			selection.removeAllRanges();
 			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
-			renderer.markToSync( 'TEXT', viewText );
+			renderer.markToSync( 'text', viewText );
 			renderer.render();
 
 			expect( domB.childNodes.length ).to.equal( 1 );
@@ -737,7 +737,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -754,7 +754,7 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).endContainer ).to.equal( domP.childNodes[ 1 ].childNodes[ 0 ] );
 			expect( domSelection.getRangeAt( 0 ).endOffset ).to.equal( 1 );
 
-			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
@@ -766,7 +766,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domP = domRoot.childNodes[ 0 ];
@@ -787,7 +787,7 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
 			const domB = domRoot.childNodes[ 0 ].childNodes[ 1 ];
@@ -799,7 +799,7 @@ describe( 'Renderer', () => {
 			domB.childNodes[ 0 ].data = '';
 
 			selection.removeAllRanges();
-			renderer.markToSync( 'CHILDREN', viewB );
+			renderer.markToSync( 'children', viewB );
 
 			expect( () => {
 				renderer.render();

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

@@ -0,0 +1,84 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view */
+
+'use strict';
+
+import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import EditableElement from '/ckeditor5/engine/view/editableelement.js';
+import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
+
+describe( 'RootEditableElement', () => {
+	describe( 'constructor', () => {
+		it( 'should create an element with default root name', () => {
+			const root = new RootEditableElement( {}, 'div' );
+
+			expect( root ).to.be.instanceof( EditableElement );
+			expect( root ).to.be.instanceof( ContainerElement );
+
+			expect( root.rootName ).to.equal( 'main' );
+			expect( root.name ).to.equal( 'div' );
+
+			expect( root.isFocused ).to.be.false;
+			expect( root.isReadOnly ).to.be.false;
+		} );
+
+		it( 'should create an element with custom root name', () => {
+			const root = new RootEditableElement( {}, 'h1', 'header' );
+
+			expect( root.rootName ).to.equal( 'header' );
+			expect( root.name ).to.equal( 'h1' );
+
+			expect( root.isFocused ).to.be.false;
+			expect( root.isReadOnly ).to.be.false;
+		} );
+	} );
+
+	describe( 'isFocused', () => {
+		it( 'should be observable', () => {
+			const root = new RootEditableElement( {}, 'div' );
+
+			expect( root.isFocused ).to.be.false;
+
+			const isFocusedSpy = sinon.spy();
+
+			root.on( 'change:isFocused', isFocusedSpy );
+
+			root.isFocused = true;
+
+			expect( root.isFocused ).to.be.true;
+
+			expect( isFocusedSpy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'isReadOnly', () => {
+		it( 'should be observable', () => {
+			const root = new RootEditableElement( {}, 'div' );
+
+			expect( root.isReadOnly ).to.be.false;
+
+			const isReadOnlySpy = sinon.spy();
+
+			root.on( 'change:isReadOnly', isReadOnlySpy );
+
+			root.isReadOnly = true;
+
+			expect( root.isReadOnly ).to.be.true;
+
+			expect( isReadOnlySpy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'getDocument', ()=> {
+		it( 'should return document', () => {
+			const docMock = {};
+			const root = new RootEditableElement( docMock, 'div' );
+
+			expect( root.getDocument() ).to.equal( docMock );
+		} );
+	} );
+} );