Forráskód Böngészése

Merge pull request #261 from ckeditor/t/243

t/243: TreeView should handle multiple roots.
Piotrek Koszuliński 9 éve
szülő
commit
1573d5bf28

+ 109 - 0
packages/ckeditor5-engine/src/treeview/observer/domeventobserver.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Base class for DOM event observers. This class handles
+ * {@link core.treeView.observer.Observer#observe adding} listeners to DOM elements,
+ * {@link core.treeView.observer.Observer#disable disabling} and
+ * {@link core.treeView.observer.Observer#enable re-enabling} events.
+ * Child class needs to define
+ * {@link core.treeView.observer.DomEventObserver#domEventType DOM event type} and
+ * {@link core.treeView.observer.DomEventObserver#onDomEvent callback}.
+ *
+ * For instance:
+ *
+ *		class ClickObserver extends DomEventObserver {
+ *			// It can also be defined as a normal property in the constructor.
+ *			get domEventType() {
+ *				return 'click';
+ *			}
+ *
+ *			onDomEvent( domEvt ) {
+ *				this.fire( 'click' );
+ *			}
+ *		}
+ *
+ * Or:
+ *
+ *		const observer = new DomEventObserver();
+ *		observer.domEventType = 'click';
+ *		observer.onDomEvent = ( domEvt ) => observer.fire( 'click' );
+ *
+ */
+export default class DomEventObserver {
+	/**
+	 * @inheritDoc
+	 */
+	init( treeView ) {
+		/**
+		 * Reference to the {@link core.treeView.TreeView} object.
+		 *
+		 * @member {core.treeView.TreeView} core.treeView.observer.DomEventObserver#treeView
+		 */
+		this.treeView = treeView;
+
+		/**
+		 * State of the observer. If it is disabled not event should be fired.
+		 *
+		 * @member {Boolean} core.treeView.observer.DomEventObserver#isEnabled
+		 */
+		this.isEnabled = false;
+
+		/**
+		 * Type of the DOM event the observer should listen on.
+		 *
+		 * @member {String} core.treeView.observer.DomEventObserver#domEventType
+		 */
+	}
+
+	/**
+	 * Callback which should be called when the DOM event occurred. Note that the callback will not be called if
+	 * observer {@link core.treeView.observer.DomEventObserver#isEnabled is not enabled}.
+	 *
+	 * @see core.treeView.observer.DomEventObserver#domEventType
+	 * @abstract
+	 * @method core.treeView.observer.DomEventObserver#onDomEvent
+	 */
+
+	/**
+	 * @inheritDoc
+	 */
+	observe( domElement ) {
+		domElement.addEventListener( this.domEventType, domEvent => {
+			if ( this.isEnabled ) {
+				this.onDomEvent( domEvent );
+			}
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	enable() {
+		this.isEnabled = true;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	disable() {
+		this.isEnabled = false;
+	}
+
+	/**
+	 * Calls {@link core.treeView.TreeView#fire} if observer
+	 * {@link core.treeView.observer.DomEventObserver#isEnabled is enabled}.
+	 *
+	 * @see core.treeView.TreeView#fire
+	 * @param {...*} args Fire arguments {@link core.treeView.TreeView#fire}.
+	 */
+	fire( ...args ) {
+		if ( this.isEnabled ) {
+			this.treeView.fire.apply( this.treeView, args );
+		}
+	}
+}

+ 22 - 10
packages/ckeditor5-engine/src/treeview/observer/mutationobserver.js

@@ -54,13 +54,6 @@ export default class MutationObserver extends Observer {
 		 */
 		this.treeView = treeView;
 
-		/**
-		 * Reference to the {@link core.treeView.TreeView#domRoot}.
-		 *
-		 * @member {HTMLElement} core.treeView.observer.MutationObserver#domRoot
-		 */
-		this.domRoot = treeView.domRoot;
-
 		/**
 		 * Reference to the {@link core.treeView.TreeView#domConverter}.
 		 *
@@ -82,19 +75,38 @@ export default class MutationObserver extends Observer {
 		 * @member {window.MutationObserver} core.treeView.observer.MutationObserver#_mutationObserver
 		 */
 		this._mutationObserver = new window.MutationObserver( this._onMutations.bind( this ) );
+
+		this.domElements = [];
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	attach() {
-		this._mutationObserver.observe( this.domRoot, this._config );
+	observe( domElement ) {
+		this.domElements.push( domElement );
+
+		if ( this.isEnabled ) {
+			this._mutationObserver.observe( domElement, this._config );
+		}
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	detach() {
+	enable() {
+		this.isEnabled = true;
+
+		for ( let domElement of this.domElements ) {
+			this._mutationObserver.observe( domElement, this._config );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	disable() {
+		this.isEnabled = false;
+
 		this._mutationObserver.disconnect();
 	}
 

+ 23 - 11
packages/ckeditor5-engine/src/treeview/observer/observer.js

@@ -8,34 +8,46 @@
 /**
  * Abstract base observer class. Observers are classes which observe changes on DOM elements, do the preliminary
  * processing and fire events on the {@link core.treeView.TreeView} objects.
+ *
  * @abstract
  * @memberOf core.treeView.observer
  */
 export default class Observer {
 	/**
-	 * Inits the observer class, caches all needed {@link core.treeView.TreeView} properties, create objects.
-	 * This method do not {@link core.treeView.observer.Observer#attach attach} listeners to the DOM.
+	 * Inits the observer class, caches all needed {@link core.treeView.TreeView} properties.
+	 * After being inited, the observer will be {@link core.treeView.observer.Observer#enabled enabled}.
 	 *
 	 * @method core.treeView.observer.Observer#init
 	 * @param {core.treeView.TreeView}
 	 */
 
 	/**
-	 * Attaches observers and listeners to DOM elements. This method is called when then observer is added to the
-	 * {@link core.treeView.TreeView} and after {@link core.treeView.TreeView#render rendering} to reattach observers and listeners.
+	 * Enables the observer. This method is called when then observer is registered to the
+	 * {@link core.treeView.TreeView} and after {@link core.treeView.TreeView#render rendering}
+	 * (all observers are {@link core.treeView.observer.Observer#disable disabled} before rendering).
 	 *
-	 * @see core.treeView.observer.Observer#detach
+	 * A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
+	 * However, a child class may not need to be disabled, so it can implement an empty method.
 	 *
-	 * @method core.treeView.observer.Observer#attach
+	 * @see core.treeView.observer.Observer#disable
+	 *
+	 * @method core.treeView.observer.Observer#enable
 	 */
 
 	/**
-	 * Detaches observers and listeners from the DOM elements. This method is called before
-	 * {@link core.treeView.TreeView#render rendering} to prevent firing events during rendering and when the editor is
-	 * destroyed.
+	 * Disables the observer. This method is called before
+	 * {@link core.treeView.TreeView#render rendering} to prevent firing events during rendering.
+	 *
+	 * @see core.treeView.observer.Observer#enable
 	 *
-	 * @see core.treeView.observer.Observer#attach
+	 * @method core.treeView.observer.Observer#disable
+	 */
+
+	/**
+	 * Starts observing the given root element.
 	 *
-	 * @method core.treeView.observer.Observer#detach
+	 * @method core.treeView.observer.Observer#observe
+	 * @param {HTMLElement} domElement
+	 * @param {String} name The name of the root element.
 	 */
 }

+ 58 - 30
packages/ckeditor5-engine/src/treeview/treeview.js

@@ -13,38 +13,34 @@ import Writer from './writer.js';
 import utils from '../../utils/utils.js';
 
 /**
- * TreeView class combines the actual tree of view elements, tree of DOM elements,
+ * TreeView class creates an abstract layer over the content editable area.
+ * It combines the actual tree of view elements, tree of DOM elements,
  * {@link core.treeView.DomConverter DOM Converter}, {@link core.treeView.Renderer renderer} and all
- * {@link core.treeView.Observer observers}. It creates an abstract layer over the content editable area.
+ * {@link core.treeView.Observer observers}.
  *
  * If you want to only transform the tree of view elements to the DOM elements you can use the
  * {@link core.treeView.DomConverter DomConverter}.
  *
  * @memberOf core.treeView
- * @mixes core.EmitterMixin
+ * @mixes utils.EmitterMixin
  */
 export default class TreeView {
 	/**
-	 * Creates a TreeView based on the HTMLElement.
-	 *
-	 * The constructor copies the element name and attributes to create the
-	 * root of the view, but does not copy its children. This means that the while rendering, the whole content of this
-	 * root element will be removed when you call {@link core.treeView.TreeView#render render} but the root name and
-	 * attributes will be preserved.
-	 *
-	 * @param {HTMLElement} domRoot DOM element in which the tree view should do change.
+	 * Creates a TreeView instance.
 	 */
-	constructor( domRoot ) {
+	constructor() {
 		/**
-		 * Root of the DOM tree.
+		 * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
 		 *
-		 * @member {HTMLElement} core.treeView.TreeView#domRoot
+		 * @readonly
+		 * @member {Map} core.treeView.TreeView#domRoots
 		 */
-		this.domRoot = domRoot;
+		this.domRoots = new Map();
 
 		/**
 		 * Set of {@link core.treeView.Observer observers}.
 		 *
+		 * @readonly
 		 * @member {Set.<core.treeView.Observer>} core.treeView.TreeView#observers
 		 */
 		this.observers = new Set();
@@ -52,6 +48,7 @@ export default class TreeView {
 		/**
 		 * Tree View writer.
 		 *
+		 * @readonly
 		 * @member {core.treeView.Writer} core.treeView.TreeView#writer
 		 */
 		this.writer = new Writer();
@@ -60,42 +57,73 @@ export default class TreeView {
 		 * Instance of the {@link core.treeView.DomConverter domConverter} use by
 		 * {@link core.treeView.TreeView#renderer renderer} and {@link core.treeView.TreeView#observers observers}.
 		 *
+		 * @readonly
 		 * @member {core.treeView.DomConverter} core.treeView.TreeView#domConverter
 		 */
 		this.domConverter = new DomConverter();
 
 		/**
-		 * Root of the view tree.
+		 * Roots of the view tree. Map of the {core.treeView.Element view elements} with roots names as keys.
 		 *
-		 * @member {core.treeView.Element} core.treeView.TreeView#viewRoot
+		 * @readonly
+		 * @member {Map} core.treeView.TreeView#viewRoots
 		 */
-		this.viewRoot = this.domConverter.domToView( domRoot, { bind: true, withChildren: false } );
-		this.viewRoot.setTreeView( this );
+		this.viewRoots = new Map();
 
 		/**
 		 * Instance of the {@link core.treeView.TreeView#renderer renderer}.
 		 *
+		 * @readonly
 		 * @member {core.treeView.Renderer} core.treeView.TreeView#renderer
 		 */
 		this.renderer = new Renderer( this.domConverter );
-		this.renderer.markToSync( 'CHILDREN', this.viewRoot );
-
-		// Mark changed nodes in the renderer.
-		this.viewRoot.on( 'change', ( evt, type, node ) => {
-			this.renderer.markToSync( type, node );
-		} );
 	}
 
 	/**
 	 * Adds an observer to the set of observers. This method also {@link core.treeView.Observer#init initializes} and
-	 * {@link core.treeView.Observer#attach attaches} the observer.
+	 * {@link core.treeView.Observer#enable enables} the observer.
 	 *
-	 * @param {core.treeView.Observer} observer Observer to add.
+	 * @param {core.treeView.Observer} observer The observer to add.
 	 */
 	addObserver( observer ) {
 		this.observers.add( observer );
 		observer.init( this );
-		observer.attach();
+
+		for ( let [ name, domElement ] of this.domRoots ) {
+			observer.observe( domElement, name );
+		}
+
+		observer.enable();
+	}
+
+	/**
+	 * Creates a root for the HTMLElement. It adds elements to {@link core.treeView.TreeView#domRoots} and
+	 * {@link core.treeView.TreeView#viewRoots}.
+	 *
+	 * The constructor copies the element name and attributes to create the
+	 * root of the view, but does not copy its children. This means that while
+	 * {@link core.treeView.TreeView#render rendering}, the whole content of this
+	 * root element will be removed but the root name and attributes will be preserved.
+	 *
+	 * @param {HTMLElement} domRoot DOM element in which the tree view should do change.
+	 * @param {String} name Name of the root.
+	 */
+	createRoot( domRoot, name ) {
+		const viewRoot = this.domConverter.domToView( domRoot, { bind: true, withChildren: false } );
+		viewRoot.setTreeView( this );
+
+		// Mark changed nodes in the renderer.
+		viewRoot.on( 'change', ( evt, type, node ) => {
+			this.renderer.markToSync( type, node );
+		} );
+		this.renderer.markToSync( 'CHILDREN', viewRoot );
+
+		this.domRoots.set( name, domRoot );
+		this.viewRoots.set( name, viewRoot );
+
+		for ( let observer of this.observers ) {
+			observer.observe( domRoot, name );
+		}
 	}
 
 	/**
@@ -104,13 +132,13 @@ export default class TreeView {
 	 */
 	render() {
 		for ( let observer of this.observers ) {
-			observer.detach();
+			observer.disable();
 		}
 
 		this.renderer.render();
 
 		for ( let observer of this.observers ) {
-			observer.attach();
+			observer.enable();
 		}
 	}
 }

+ 6 - 3
packages/ckeditor5-engine/tests/treeview/integration.js

@@ -17,7 +17,8 @@ describe( 'TreeView integration', () => {
 		domDiv.setAttribute( 'id', 'editor' );
 		domDiv.appendChild( domP );
 
-		const treeView = new TreeView( domDiv );
+		const treeView = new TreeView();
+		treeView.createRoot( domDiv, 'editor' );
 		treeView.render();
 
 		expect( domDiv.childNodes.length ).to.equal( 0 );
@@ -27,8 +28,10 @@ describe( 'TreeView integration', () => {
 	it( 'should render changes in the TreeView', () => {
 		const domDiv = document.createElement( 'div' );
 
-		const treeView = new TreeView( domDiv );
-		treeView.viewRoot.appendChildren( new TreeElement( 'p' ) );
+		const treeView = new TreeView();
+		treeView.createRoot( domDiv, 'editor' );
+
+		treeView.viewRoots.get( 'editor' ).appendChildren( new TreeElement( 'p' ) );
 		treeView.render();
 
 		expect( domDiv.childNodes.length ).to.equal( 1 );

+ 5 - 0
packages/ckeditor5-engine/tests/treeview/manual/clickobserver.html

@@ -0,0 +1,5 @@
+<button id="clickerA">Clicker A</button>
+<button id="clickerB">Clicker B</button>
+
+<button id="enable1">Enable event 1</button>
+<button id="disable1">Disable event 1</button>

+ 42 - 0
packages/ckeditor5-engine/tests/treeview/manual/clickobserver.js

@@ -0,0 +1,42 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console:false */
+
+'use strict';
+
+import TreeView from '/ckeditor5/core/treeview/treeview.js';
+import DomEventObserver from '/ckeditor5/core/treeview/observer/domeventobserver.js';
+
+const treeView = new TreeView();
+
+class ClickObserver extends DomEventObserver {
+	constructor( id ) {
+		super();
+
+		this.id = id;
+		this.domEventType = 'click';
+	}
+
+	onDomEvent( domEvt ) {
+		this.fire( 'click', this.id, domEvt.target.id );
+	}
+}
+
+const observer1 = new ClickObserver( 1 );
+const observer2 = new ClickObserver( 2 );
+
+treeView.on( 'click', ( evt, eventId, elementId ) => console.log( 'click', eventId, elementId ) );
+document.getElementById( 'enable1' ).addEventListener( 'click', () => observer1.enable() );
+document.getElementById( 'disable1' ).addEventListener( 'click', () => observer1.disable() );
+
+// Random order
+treeView.addObserver( observer1 );
+
+treeView.createRoot( document.getElementById( 'clickerA' ), 'clickerA' );
+
+treeView.addObserver( observer2 );
+
+treeView.createRoot( document.getElementById( 'clickerB' ), 'clickerB' );

+ 11 - 0
packages/ckeditor5-engine/tests/treeview/manual/clickobserver.md

@@ -0,0 +1,11 @@
+@bender-ui: collapsed
+@bender-tags: treeview
+
+ - Open console.
+ - Click Clicker A.
+ - 2 events should be fired.
+ - Click Clicker B.
+ - 2 events should be fired.
+ - disable event "1".
+ - Click Clicker A.
+ - 1 event should be fired.

+ 3 - 2
packages/ckeditor5-engine/tests/treeview/manual/init.js

@@ -11,9 +11,10 @@ import TreeView from '/ckeditor5/core/treeview/treeview.js';
 import Element from '/ckeditor5/core/treeview/element.js';
 import Text from '/ckeditor5/core/treeview/text.js';
 
-const treeView = new TreeView( document.getElementById( 'editor' ) );
+const treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
 
-treeView.viewRoot.insertChildren( 0, [
+treeView.viewRoots.get( 'editor' ).appendChildren( [
 	new Element( 'p', [], [ new Text( 'New' ) ] ),
 	new Element( 'p', [], [ new Text( 'Content' ) ] )
 ] );

+ 3 - 2
packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.js

@@ -12,14 +12,15 @@ import Element from '/ckeditor5/core/treeview/element.js';
 import Text from '/ckeditor5/core/treeview/text.js';
 import MutationObserver from '/ckeditor5/core/treeview/observer/mutationobserver.js';
 
-const treeView = new TreeView( document.getElementById( 'editor' ) );
+const treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
 const mutationObserver = new MutationObserver();
 
 treeView.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 
 treeView.addObserver( mutationObserver );
 
-treeView.viewRoot.insertChildren( 0, [
+treeView.viewRoots.get( 'editor' ).appendChildren( [
 	new Element( 'p', [], [ new Text( 'foo' ) ] ),
 	new Element( 'p', [], [ new Text( 'bom' ) ] )
 ] );

+ 3 - 2
packages/ckeditor5-engine/tests/treeview/manual/typing.js

@@ -12,14 +12,15 @@ import Element from '/ckeditor5/core/treeview/element.js';
 import Text from '/ckeditor5/core/treeview/text.js';
 import MutationObserver from '/ckeditor5/core/treeview/observer/mutationobserver.js';
 
-const treeView = new TreeView( document.getElementById( 'editor' ) );
+const treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
 
 treeView.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 treeView.on( 'mutations', handleTyping );
 
 treeView.addObserver( new MutationObserver() );
 
-treeView.viewRoot.insertChildren( 0, [ new Element( 'p', [], [ new Text( 'foo' ) ] ) ] );
+treeView.viewRoots.get( 'editor' ).appendChildren( [ new Element( 'p', [], [ new Text( 'foo' ) ] ) ] );
 
 treeView.render();
 

+ 109 - 0
packages/ckeditor5-engine/tests/treeview/observer/domeventobserver.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview */
+
+'use strict';
+
+import DomEventObserver from '/ckeditor5/core/treeview/observer/domeventobserver.js';
+import TreeView from '/ckeditor5/core/treeview/treeview.js';
+
+class TestObserver extends DomEventObserver {
+	constructor( id ) {
+		super();
+
+		this.id = id;
+
+		this.domEventType = 'click';
+	}
+
+	onDomEvent( domEvt ) {
+		this.fire( 'click', this.id, domEvt );
+	}
+}
+
+describe( 'DomEventObserver', () => {
+	it( 'should add event lister', () => {
+		const testObserver = new TestObserver( 'foo' );
+		const domElement = document.createElement( 'p' );
+		const domEvent = new MouseEvent( 'click' );
+		const treeView = new TreeView();
+		const evtSpy = sinon.spy();
+
+		treeView.createRoot( domElement, 'root' );
+		treeView.addObserver( testObserver );
+		treeView.on( 'click', evtSpy );
+
+		domElement.dispatchEvent( domEvent );
+
+		expect( evtSpy.calledOnce ).to.be.true;
+		expect( evtSpy.args[ 0 ][ 1 ] ).to.equal( 'foo' );
+		expect( evtSpy.args[ 0 ][ 2 ] ).to.equal( domEvent );
+	} );
+
+	it( 'should not fire event if observer is disabled', () => {
+		const testObserver = new TestObserver( 'foo' );
+		const domElement = document.createElement( 'p' );
+		const domEvent = new MouseEvent( 'click' );
+		const treeView = new TreeView();
+		const evtSpy = sinon.spy();
+
+		treeView.createRoot( domElement, 'root' );
+		treeView.addObserver( testObserver );
+		treeView.on( 'click', evtSpy );
+
+		testObserver.disable();
+
+		domElement.dispatchEvent( domEvent );
+
+		expect( evtSpy.called ).to.be.false;
+	} );
+
+	it( 'should be fire event if observer is disabled and re-enabled', () => {
+		const testObserver = new TestObserver( 'foo' );
+		const domElement = document.createElement( 'p' );
+		const domEvent = new MouseEvent( 'click' );
+		const treeView = new TreeView();
+		const evtSpy = sinon.spy();
+
+		treeView.createRoot( domElement, 'root' );
+		treeView.addObserver( testObserver );
+		treeView.on( 'click', evtSpy );
+
+		testObserver.disable();
+
+		domElement.dispatchEvent( domEvent );
+
+		expect( evtSpy.called ).to.be.false;
+
+		testObserver.enable();
+
+		domElement.dispatchEvent( domEvent );
+
+		expect( evtSpy.calledOnce ).to.be.true;
+	} );
+
+	describe( 'fire', () => {
+		it( 'should do nothing if observer is disabled', () => {
+			const testObserver = new TestObserver();
+			const treeView = new TreeView();
+			const fireSpy = sinon.spy( treeView, 'fire' );
+
+			testObserver.init( treeView );
+
+			testObserver.disable();
+
+			testObserver.fire( 'click' );
+
+			expect( fireSpy.called ).to.be.false;
+
+			testObserver.enable();
+
+			testObserver.fire( 'click' );
+
+			expect( fireSpy.calledOnce ).to.be.true;
+		} );
+	} );
+} );

+ 2 - 1
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.html

@@ -1 +1,2 @@
-<div contenteditable="true" id="editor"></div>
+<div contenteditable="true" id="editor"></div>
+<div contenteditable="true" id="editor2"></div>

+ 35 - 9
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js

@@ -13,27 +13,31 @@ import Text from '/ckeditor5/core/treeview/text.js';
 import MutationObserver from '/ckeditor5/core/treeview/observer/mutationobserver.js';
 
 describe( 'MutationObserver', () => {
-	let domEditor, treeView, mutationObserver, lastMutations;
+	let domEditor, treeView, viewRoot, mutationObserver, lastMutations;
 
 	beforeEach( () => {
+		treeView = new TreeView();
 		domEditor = document.getElementById( 'editor' );
-		treeView = new TreeView( domEditor );
 		mutationObserver = new MutationObserver();
 		lastMutations = null;
 
+		treeView.createRoot( domEditor, 'editor' );
+
 		treeView.addObserver( mutationObserver );
 		treeView.on( 'mutations', ( evt, mutations ) => lastMutations = mutations );
 
-		treeView.viewRoot.insertChildren( 0, [
+		viewRoot = treeView.viewRoots.get( 'editor' );
+
+		viewRoot.insertChildren( 0, [
 			new Element( 'p', [], [ new Text( 'foo' ) ] ),
 			new Element( 'p', [], [ new Text( 'bar' ) ] )
-			] );
+		] );
 
 		treeView.render();
 	} );
 
 	afterEach( () => {
-		mutationObserver.detach();
+		mutationObserver.disable();
 	} );
 
 	it( 'should handle typing', () => {
@@ -44,7 +48,7 @@ describe( 'MutationObserver', () => {
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
 		expect( lastMutations[ 0 ].type ).to.equal( 'text' );
-		expect( lastMutations[ 0 ].node ).to.equal( treeView.viewRoot.getChild( 0 ).getChild( 0 ) );
+		expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ).getChild( 0 ) );
 		expect( lastMutations[ 0 ].newText ).to.equal( 'foom' );
 		expect( lastMutations[ 0 ].oldText ).to.equal( 'foo' );
 	} );
@@ -60,7 +64,7 @@ describe( 'MutationObserver', () => {
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
 		expect( lastMutations[ 0 ].type ).to.equal( 'children' );
-		expect( lastMutations[ 0 ].node ).to.equal( treeView.viewRoot.getChild( 0 ) );
+		expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ) );
 
 		expect( lastMutations[ 0 ].newChildren.length ).to.equal( 2 );
 		expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'f' );
@@ -79,7 +83,7 @@ describe( 'MutationObserver', () => {
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
 		expect( lastMutations[ 0 ].type ).to.equal( 'text' );
-		expect( lastMutations[ 0 ].node ).to.equal( treeView.viewRoot.getChild( 0 ).getChild( 0 ) );
+		expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ).getChild( 0 ) );
 		expect( lastMutations[ 0 ].newText ).to.equal( 'fooxy' );
 		expect( lastMutations[ 0 ].oldText ).to.equal( 'foo' );
 	} );
@@ -98,7 +102,29 @@ describe( 'MutationObserver', () => {
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
 		expect( lastMutations[ 0 ].type ).to.equal( 'children' );
-		expect( lastMutations[ 0 ].node ).to.equal( treeView.viewRoot );
+		expect( lastMutations[ 0 ].node ).to.equal( viewRoot );
+	} );
+
+	it( 'should be able to observe multiple roots', () => {
+		const domEditor2 = document.getElementById( 'editor2' );
+
+		// Prepare editor2
+		treeView.createRoot( domEditor2, 'editor2' );
+
+		treeView.viewRoots.get( 'editor2' ).insertChildren( 0, [
+			new Element( 'p', [], [ new Text( 'foo' ) ] ),
+			new Element( 'p', [], [ new Text( 'bar' ) ] )
+		] );
+
+		// Render editor2 (first editor has been rendered in the beforeEach function)
+		treeView.render();
+
+		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
+		domEditor2.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
+
+		handleMutation();
+
+		expect( lastMutations.length ).to.equal( 2 );
 	} );
 
 	function handleMutation() {

+ 37 - 17
packages/ckeditor5-engine/tests/treeview/treeview.js

@@ -10,28 +10,48 @@
 import TreeView from '/ckeditor5/core/treeview/treeview.js';
 import Observer from '/ckeditor5/core/treeview/observer/observer.js';
 import Renderer from '/ckeditor5/core/treeview/renderer.js';
+import Writer from '/ckeditor5/core/treeview/writer.js';
 import DomConverter from '/ckeditor5/core/treeview/domconverter.js';
 
+import utils from '/ckeditor5/utils/utils.js';
+
+const count = utils.count;
+
 describe( 'TreeView', () => {
 	describe( 'constructor', () => {
 		it( 'should create TreeView with all properties', () => {
+			const treeView = new TreeView();
+
+			expect( count( treeView.domRoots ) ).to.equal( 0 );
+			expect( count( treeView.viewRoots ) ).to.equal( 0 );
+			expect( count( treeView.observers ) ).to.equal( 0 );
+			expect( treeView ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
+			expect( treeView ).to.have.property( 'writer' ).that.is.instanceOf( Writer );
+			expect( treeView ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
+		} );
+	} );
+
+	describe( 'createRoot', () => {
+		it( 'should create root', () => {
 			const domP = document.createElement( 'p' );
 			const domDiv = document.createElement( 'div' );
 			domDiv.setAttribute( 'id', 'editor' );
 			domDiv.appendChild( domP );
 
-			const treeView = new TreeView( domDiv );
+			const treeView = new TreeView();
+			treeView.createRoot( domDiv, 'editor' );
 
-			expect( treeView ).to.have.property( 'domRoot' ).that.equals( domDiv );
-			expect( treeView ).to.have.property( 'observers' ).that.is.instanceOf( Set );
-			expect( treeView ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
-			expect( treeView ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
-			expect( treeView ).to.have.property( 'viewRoot' );
+			expect( count( treeView.domRoots ) ).to.equal( 1 );
+			expect( count( treeView.viewRoots ) ).to.equal( 1 );
+
+			const domRoot = treeView.domRoots.get( 'editor' );
+			const viewRoot = treeView.viewRoots.get( 'editor' );
 
-			expect( treeView.domConverter.getCorrespondingDom( treeView.viewRoot ) ).to.equal( domDiv );
-			expect( treeView.viewRoot.name ).to.equal( 'div' );
-			expect( treeView.viewRoot.getAttribute( 'id' ) ).to.equal( 'editor' );
-			expect( treeView.renderer.markedChildren.has( treeView.viewRoot ) ).to.be.true;
+			expect( domRoot ).to.equal( domDiv );
+			expect( treeView.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
+			expect( viewRoot.name ).to.equal( 'div' );
+			expect( viewRoot.getAttribute( 'id' ) ).to.equal( 'editor' );
+			expect( treeView.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 		} );
 	} );
 
@@ -40,31 +60,31 @@ describe( 'TreeView', () => {
 
 		beforeEach( () => {
 			observerMock = new Observer();
-			observerMock.attach = sinon.spy();
-			observerMock.detach = sinon.spy();
+			observerMock.enable = sinon.spy();
+			observerMock.disable = sinon.spy();
 			observerMock.init = sinon.spy();
 
 			treeView = new TreeView( document.createElement( 'div' ) );
 			treeView.renderer.render = sinon.spy();
 		} );
 
-		it( 'should be inited and attached on adding', () => {
+		it( 'should be inited and enabled on adding', () => {
 			treeView.addObserver( observerMock );
 
 			expect( treeView.observers.has( observerMock ) ).to.be.true;
 			sinon.assert.calledOnce( observerMock.init );
 			sinon.assert.calledWith( observerMock.init, treeView );
-			sinon.assert.calledOnce( observerMock.attach );
+			sinon.assert.calledOnce( observerMock.enable );
 		} );
 
-		it( 'should be detached and reattached on render', () => {
+		it( 'should be disabled and re-enabled on render', () => {
 			treeView.addObserver( observerMock );
 			treeView.render();
 
 			expect( treeView.observers.has( observerMock ) ).to.be.true;
-			sinon.assert.calledOnce( observerMock.detach );
+			sinon.assert.calledOnce( observerMock.disable );
 			sinon.assert.calledOnce( treeView.renderer.render );
-			sinon.assert.calledTwice( observerMock.attach );
+			sinon.assert.calledTwice( observerMock.enable );
 		} );
 	} );
 } );