소스 검색

A brand new algorithm of UI initialisation. Defined View#regions as a plain object.

Aleksander Nowodzinski 10 년 전
부모
커밋
d4b076772f
3개의 변경된 파일237개의 추가작업 그리고 68개의 파일을 삭제
  1. 69 33
      packages/ckeditor5-engine/src/ui/controller.js
  2. 97 17
      packages/ckeditor5-engine/src/ui/region.js
  3. 71 18
      packages/ckeditor5-engine/src/ui/view.js

+ 69 - 33
packages/ckeditor5-engine/src/ui/controller.js

@@ -7,8 +7,9 @@
 
 CKEDITOR.define( [
 	'collection',
-	'model'
-], function( Collection, Model ) {
+	'model',
+	'ckeditorerror',
+], function( Collection, Model, CKEditorError ) {
 	class Controller extends Model {
 		/**
 		 * @constructor
@@ -26,6 +27,10 @@ CKEDITOR.define( [
 			 */
 			this.view = view;
 
+			/**
+			 * @property {Boolean} ready
+			 */
+
 			/**
 			 * Collections of child controller regions.
 			 */
@@ -39,20 +44,39 @@ CKEDITOR.define( [
 		 * @returns
 		 */
 		init() {
-			// Note: Because this.view.init() can by sync as well as async,
-			// this method is not returning this.view.init() directly.
+			if ( this.ready ) {
+				/**
+				 * This Controller has already been initialized.
+				 *
+				 * @error ui-controller-init
+				 * @param {Controller} controller
+				 */
+				throw new CKEditorError(
+					'ui-controller-init: This Controller has already been initialized.',
+					{ view: this }
+				);
+			}
+
 			return Promise.resolve()
+				// Note: Because this.view.init() can be sync as well as async,
+				// this method is not returning this.view.init() directly.
 				.then( () => {
 					return this.view.init();
 				} )
 				.then( () => {
 					let promises = [];
+					let region, controller;
 
-					for ( let region of this.regions ) {
-						promises.concat( region.map( c => c.init() ) );
+					for ( region of this.regions ) {
+						for ( controller of region ) {
+							promises.push( controller.init() );
+						}
 					}
 
 					return Promise.all( promises );
+				} )
+				.then( () => {
+					this.ready = true;
 				} );
 		}
 
@@ -60,7 +84,7 @@ CKEDITOR.define( [
 		 * @param
 		 * @returns
 		 */
-		add( controller, regionName ) {
+		addChild( controller, regionName ) {
 			let region = this.regions.get( regionName );
 
 			if ( !region ) {
@@ -69,7 +93,33 @@ CKEDITOR.define( [
 
 			region.add( controller );
 
-			return Promise.resolve( controller );
+			// If this Controller has already been inited, then every single new
+			// child controller must be inited separately when added.
+			if ( this.ready ) {
+				return controller.init();
+			}
+			// If this Controller.init() hasn't been called yet, then the child
+			// controller will be initialized by init().
+			else {
+				return Promise.resolve();
+			}
+		}
+
+		_createRegion( regionName ) {
+			const region = new Collection();
+			region.name = regionName;
+
+			region.on( 'add', ( evt, controller, index ) => {
+				this.view.addChild( controller.view, regionName, index );
+			} );
+
+			region.on( 'remove', ( evt, controller ) => {
+				this.view.removeChild( controller.view, regionName );
+			} );
+
+			this.regions.add( region );
+
+			return region;
 		}
 
 		/**
@@ -77,39 +127,25 @@ CKEDITOR.define( [
 		 * @returns
 		 */
 		destroy() {
-			// Note: Because this.view.destroy() can by sync as well as async,
-			// it is wrapped in promise.
 			return Promise.resolve()
-				.then( () => {
-					return this.view.destroy();
-				} )
 				.then( () => {
 					let promises = [];
+					let region, controller;
 
-					this.regions.forEach( region => {
-						region.forEach( controller => promises.push( controller.destroy() ) );
-					} );
+					for ( region of this.regions ) {
+						for ( controller of this.regions.remove( region ) ) {
+							promises.push( region.remove( controller ).destroy() );
+						}
+					}
 
 					return Promise.all( promises );
+				} )
+				// Note: Because this.view.destroy() can be sync as well as async,
+				// it is wrapped in promise.
+				.then( () => {
+					return this.view.destroy();
 				} );
 		}
-
-		_createRegion( regionName ) {
-			let controllers = new Collection();
-			controllers.name = regionName;
-
-			controllers.on( 'add', ( evt, controller, index ) => {
-				this.view.add( controller.view, regionName, index );
-			} );
-
-			controllers.on( 'remove', ( evt, controller ) => {
-				this.view.remove( controller.view, regionName );
-			} );
-
-			this.regions.add( controllers );
-
-			return controllers;
-		}
 	}
 
 	return Controller;

+ 97 - 17
packages/ckeditor5-engine/src/ui/region.js

@@ -12,7 +12,11 @@
  * @extends Model
  */
 
-CKEDITOR.define( [ 'collection', 'model' ], ( Collection, Model ) => {
+CKEDITOR.define( [
+	'collection',
+	'model',
+	'ckeditorerror',
+], ( Collection, Model, CKEditorError ) => {
 	class Region extends Model {
 		/**
 		 * Creates an instance of the {@link Region} class.
@@ -21,7 +25,7 @@ CKEDITOR.define( [ 'collection', 'model' ], ( Collection, Model ) => {
 		 * @param {HTMLElement} [el] The element used for this region.
 		 * @constructor
 		 */
-		constructor( name, el ) {
+		constructor( name, elDef ) {
 			super();
 
 			/**
@@ -30,15 +34,88 @@ CKEDITOR.define( [ 'collection', 'model' ], ( Collection, Model ) => {
 			this.name = name;
 
 			/**
-			 * The element of the region.
+			 * @property {HTMLElement} _elDef
 			 */
-			this.el = el;
+			this._elDef = elDef;
 
 			/**
 			 * Views which belong to the region.
 			 */
 			this.views = new Collection();
 
+			/**
+			 * @property {View} parent
+			 */
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		init( parent ) {
+			this.parent = parent;
+
+			if ( this.el ) {
+				this._initChildViews();
+			}
+		}
+
+		/**
+		 * Element of this Region. The element is rendered on first reference.
+		 *
+		 * @property el
+		 */
+		get el() {
+			return this._el || this._getElement();
+		}
+
+		set el( el ) {
+			this._el = el;
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		_getElement() {
+			const elDef = this._elDef;
+			let el;
+
+			if ( typeof elDef == 'string' ) {
+				el = this.parent.el.querySelector( elDef );
+			} else if ( typeof elDef == 'function' ) {
+				el = elDef( this.parent.el );
+			} else if ( elDef === true ) {
+				el = null;
+			} else {
+				/**
+				 * Region definition must be either `Function`, `String` or `Boolean` (`true`).
+				 *
+				 * @error ui-region-element
+				 * @param {Region} region
+				 */
+				throw new CKEditorError(
+					'ui-region-element: Region definition must be either `Function`, `String` or `Boolean` (`true`).',
+					{ region: this }
+				);
+			}
+
+			return ( this._el = el );
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		_initChildViews() {
+			let view;
+
+			// Add registered views to DOM.
+			for ( view of this.views ) {
+				this.el.appendChild( view.el );
+			}
+
+			// Attach listeners for future manipulation.
 			this.views.on( 'add', ( evt, view ) => {
 				if ( this.el ) {
 					this.el.appendChild( view.el );
@@ -50,6 +127,22 @@ CKEDITOR.define( [ 'collection', 'model' ], ( Collection, Model ) => {
 			} );
 		}
 
+		/**
+		 * @param
+		 * @returns
+		 */
+		addChild( view, index ) {
+			this.views.add( view, index );
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		removeChild( view ) {
+			this.views.remove( view );
+		}
+
 		/**
 		 * Destroys the Region instance.
 		 */
@@ -58,19 +151,6 @@ CKEDITOR.define( [ 'collection', 'model' ], ( Collection, Model ) => {
 			// Element comes as a parameter and it could be a part of the View.
 			// Then it's up to the View what to do with it when the View is destroyed.
 			this.el = null;
-
-			// Remove and destroy views.
-			for ( let view of this.views ) {
-				this.views.remove( view ).destroy();
-			}
-		}
-
-		add( view, index ) {
-			this.views.add( view, index );
-		}
-
-		remove( view ) {
-			this.views.remove( view );
 		}
 	}
 

+ 71 - 18
packages/ckeditor5-engine/src/ui/view.js

@@ -16,11 +16,12 @@
 CKEDITOR.define( [
 	'collection',
 	'model',
+	'ui/region',
 	'ui/template',
 	'ckeditorerror',
 	'ui/domemittermixin',
 	'utils'
-], ( Collection, Model, Template, CKEditorError, DOMEmitterMixin, utils ) => {
+], ( Collection, Model, Region, Template, CKEditorError, DOMEmitterMixin, utils ) => {
 	class View extends Model {
 		/**
 		 * Creates an instance of the {@link View} class.
@@ -43,6 +44,10 @@ CKEDITOR.define( [
 				idProperty: 'name'
 			} );
 
+			/**
+			 * @property {Object} regionsDef
+			 */
+
 			/**
 			 * @property {HTMLElement} _el
 			 */
@@ -61,7 +66,60 @@ CKEDITOR.define( [
 		 * @returns
 		 */
 		init() {
-			// TODO: What if we used render() here?
+			this._initRegions();
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		_initRegions() {
+			let regionName, region;
+
+			// Add regions that haven't been created yet (i.e. by addChild()).
+			// Those regions have no children Views at that point.
+			for ( regionName in this.regionsDef ) {
+				if ( !this.regions.get( regionName ) ) {
+					this._createRegion( regionName );
+				}
+			}
+
+			// Initialize regions. Feed them with an element of this View.
+			for ( region of this.regions ) {
+				region.init( this );
+			}
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		addChild( childView, regionName, index ) {
+			// Create a Region instance on demand.
+			const region = this.regions.get( regionName ) || this._createRegion( regionName );
+
+			region.addChild( childView, index );
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		removeChild( childView, regionName ) {
+			return this.regions.get( regionName ).removeChild( childView );
+		}
+
+		/**
+		 * @param
+		 * @returns
+		 */
+		_createRegion( regionName ) {
+			// Use region element definition from `View#regions`.
+			const region = new Region( regionName, this.regionsDef[ regionName ] );
+
+			this.regions.add( region );
+
+			return region;
 		}
 
 		/**
@@ -73,6 +131,10 @@ CKEDITOR.define( [
 			return this._el || this.render();
 		}
 
+		set el( el ) {
+			this._el = el;
+		}
+
 		/**
 		 * Binds a `property` of View's model so the DOM of the View is updated when the `property`
 		 * changes. It returns a function which, once called in the context of a DOM element,
@@ -134,7 +196,7 @@ CKEDITOR.define( [
 			}
 
 			// Prepare pre–defined listeners.
-			this.prepareListeners();
+			this._prepareListeners();
 
 			this._template = new Template( this.template );
 
@@ -145,6 +207,9 @@ CKEDITOR.define( [
 		 * Destroys the View.
 		 */
 		destroy() {
+			const regions = this.regions;
+			let region;
+
 			// Drop the reference to the model.
 			this.model = null;
 
@@ -154,8 +219,8 @@ CKEDITOR.define( [
 			}
 
 			// Remove and destroy regions.
-			for ( let region of this.regions ) {
-				this.regions.remove( region ).destroy();
+			for ( region of regions ) {
+				regions.remove( region ).destroy();
 			}
 
 			// Remove all listeners related to this view.
@@ -169,7 +234,7 @@ CKEDITOR.define( [
 		 *
 		 * The execution is performed by {@link Template} class.
 		 */
-		prepareListeners() {
+		_prepareListeners() {
 			if ( this.template ) {
 				this._prepareElementListeners( this.template );
 			}
@@ -269,18 +334,6 @@ CKEDITOR.define( [
 				def.children.map( this._prepareElementListeners, this );
 			}
 		}
-
-		/**
-		 * @param
-		 * @returns
-		 */
-		add( view, regionName, index ) {
-			this.regions.get( regionName ).add( view, index );
-		}
-
-		remove( view, regionName ) {
-			this.regions.get( regionName ).remove( view );
-		}
 	}
 
 	utils.extend( View.prototype, DOMEmitterMixin );