Browse Source

Support multiple roots.

Piotr Jasiun 9 years ago
parent
commit
d51dd737a1

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

@@ -55,13 +55,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}.
 		 *
 		 * @member {core.treeView.DomConverter} core.treeView.observer.MutationObserver#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();
 	}
 

+ 15 - 6
packages/ckeditor5-engine/src/treeview/observer/observer.js

@@ -14,7 +14,7 @@
 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.
+	 * This method do not {@link core.treeView.observer.Observer#enable enable} listeners to the DOM.
 	 *
 	 * @method core.treeView.observer.Observer#init
 	 * @param {core.treeView.TreeView}
@@ -22,11 +22,12 @@ export default class Observer {
 
 	/**
 	 * 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.
+	 * {@link core.treeView.TreeView} and after {@link core.treeView.TreeView#render rendering} to re-enable observers
+	 * and listeners.
 	 *
-	 * @see core.treeView.observer.Observer#detach
+	 * @see core.treeView.observer.Observer#disable
 	 *
-	 * @method core.treeView.observer.Observer#attach
+	 * @method core.treeView.observer.Observer#enable
 	 */
 
 	/**
@@ -34,8 +35,16 @@ export default class Observer {
 	 * {@link core.treeView.TreeView#render rendering} to prevent firing events during rendering and when the editor is
 	 * destroyed.
 	 *
-	 * @see core.treeView.observer.Observer#attach
+	 * @see core.treeView.observer.Observer#enable
 	 *
-	 * @method core.treeView.observer.Observer#detach
+	 * @method core.treeView.observer.Observer#disable
+	 */
+
+	/**
+	 * Adds the root HTMLElement to the observer.
+	 *
+	 * @method core.treeView.observer.Observer#observe
+	 * @param {HTMLElement} domElement
+	 * @param {String} name
 	 */
 }

+ 53 - 26
packages/ckeditor5-engine/src/treeview/treeview.js

@@ -25,26 +25,21 @@ import utils from '../../utils/utils.js';
  */
 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 class.
 	 */
-	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 +47,7 @@ export default class TreeView {
 		/**
 		 * Tree View writer.
 		 *
+		 * @readonly
 		 * @member {core.treeView.Writer} core.treeView.TreeView#writer
 		 */
 		this.writer = new Writer();
@@ -60,35 +56,31 @@ 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 on 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 enable} the observer.
 	 *
 	 * @method core.treeView.TreeView#addObserver
 	 * @param {core.treeView.Observer} observer Observer to add.
@@ -96,7 +88,42 @@ export default class TreeView {
 	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 based on 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 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.
+	 * @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 );
+		}
 	}
 
 	/**
@@ -107,13 +134,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 );

+ 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();
 

+ 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>

+ 34 - 8
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js

@@ -13,18 +13,22 @@ 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' ) ] )
 			] );
@@ -33,7 +37,7 @@ describe( 'MutationObserver', () => {
 	} );
 
 	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() {

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

@@ -10,28 +10,47 @@
 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 +59,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 enableed 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 disableed and reenableed 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 );
 		} );
 	} );
 } );