浏览代码

Merge branch 't/195'. Closes #195.

Aleksander Nowodzinski 10 年之前
父节点
当前提交
2884dafb98
共有 3 个文件被更改,包括 156 次插入73 次删除
  1. 67 36
      packages/ckeditor5-ui/src/ui/template.js
  2. 67 22
      packages/ckeditor5-ui/tests/ui/template.js
  3. 22 15
      packages/ckeditor5-ui/tests/ui/view.js

+ 67 - 36
packages/ckeditor5-ui/src/ui/template.js

@@ -7,6 +7,8 @@
 
 
 'use strict';
 'use strict';
 
 
+import CKEditorError from '../ckeditorerror.js';
+
 /**
 /**
  * Basic Template class.
  * Basic Template class.
  *
  *
@@ -17,7 +19,7 @@ export default class Template {
 	/**
 	/**
 	 * Creates an instance of the {@link Template} class.
 	 * Creates an instance of the {@link Template} class.
 	 *
 	 *
-	 * @param {TemplateDefinition} def The definition of the template.
+	 * @param {TemplateDefinition} definition The definition of the template.
 	 * @constructor
 	 * @constructor
 	 */
 	 */
 	constructor( def ) {
 	constructor( def ) {
@@ -26,20 +28,45 @@ export default class Template {
 		 *
 		 *
 		 * @property {TemplateDefinition}
 		 * @property {TemplateDefinition}
 		 */
 		 */
-		this.def = def;
+		this.definition = def;
 	}
 	}
 
 
 	/**
 	/**
-	 * Renders HTMLElement using {@link #def}.
+	 * Renders DOM Node using {@link #definition}.
 	 *
 	 *
 	 * @returns {HTMLElement}
 	 * @returns {HTMLElement}
 	 */
 	 */
 	render() {
 	render() {
-		return this._renderElement( this.def, true );
+		return this._renderNode( this.definition, true );
+	}
+
+	/**
+	 * Renders a DOM Node from definition.
+	 *
+	 * @protected
+	 * @param {TemplateDefinition} def Definition of a Node.
+	 * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
+	 * @returns {HTMLElement} A rendered Node.
+	 */
+	_renderNode( def, intoFragment ) {
+		const isText = def.text || typeof def == 'string';
+
+		// !XOR( def.tag, isText )
+		if ( def.tag ? isText : !isText ) {
+			/**
+			 * Node definition must have either "tag" or "text" property.
+			 *
+			 * @error ui-template-wrong-syntax
+			 */
+			throw new CKEditorError( 'ui-template-wrong-syntax' );
+		}
+
+		return isText ?
+			this._renderText( def ) : this._renderElement( def, intoFragment );
 	}
 	}
 
 
 	/**
 	/**
-	 * Renders an element from definition.
+	 * Renders an HTMLElement from TemplateDefinition.
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 * @param {TemplateDefinition} def Definition of an element.
 	 * @param {TemplateDefinition} def Definition of an element.
@@ -47,15 +74,8 @@ export default class Template {
 	 * @returns {HTMLElement} A rendered element.
 	 * @returns {HTMLElement} A rendered element.
 	 */
 	 */
 	_renderElement( def, intoFragment ) {
 	_renderElement( def, intoFragment ) {
-		if ( !def ) {
-			return null;
-		}
-
 		const el = document.createElement( def.tag );
 		const el = document.createElement( def.tag );
 
 
-		// Set the text first.
-		this._renderElementText( def, el );
-
 		// Set attributes.
 		// Set attributes.
 		this._renderElementAttributes( def, el );
 		this._renderElementAttributes( def, el );
 
 
@@ -70,27 +90,39 @@ export default class Template {
 			this._renderElementChildren( def, el );
 			this._renderElementChildren( def, el );
 		}
 		}
 
 
-		// Activate DOM binding for event listeners.
+		// Activate DOM bindings for event listeners.
 		this._activateElementListeners( def, el );
 		this._activateElementListeners( def, el );
 
 
 		return el;
 		return el;
 	}
 	}
 
 
 	/**
 	/**
-	 * Renders element text content from definition.
+	 * Renders a Text from TemplateDefinition or String.
 	 *
 	 *
 	 * @protected
 	 * @protected
-	 * @param {TemplateDefinition} def Definition of an element.
-	 * @param {HTMLElement} el Element which is rendered.
+	 * @param {TemplateDefinition|String} def Definition of Text or its value.
+	 * @returns {Text} A rendered Text.
 	 */
 	 */
-	_renderElementText( def, el ) {
-		if ( def.text ) {
-			if ( typeof def.text == 'function' ) {
-				def.text( el, getTextUpdater() );
-			} else {
-				el.textContent = def.text;
+	_renderText( defOrText ) {
+		const el = document.createTextNode( '' );
+
+		// Case: { text: ... }
+		if ( defOrText.text ) {
+			// Case: { text: func }, like binding
+			if ( typeof defOrText.text == 'function' ) {
+				defOrText.text( el, getTextUpdater() );
 			}
 			}
+			// Case: { text: 'foo' }
+			else {
+				el.textContent = defOrText.text;
+			}
+		}
+		// Case: 'foo'
+		else {
+			el.textContent = defOrText;
 		}
 		}
+
+		return el;
 	}
 	}
 
 
 	/**
 	/**
@@ -103,8 +135,8 @@ export default class Template {
 	_renderElementAttributes( def, el ) {
 	_renderElementAttributes( def, el ) {
 		let attr, value;
 		let attr, value;
 
 
-		for ( attr in def.attrs ) {
-			value = def.attrs[ attr ];
+		for ( attr in def.attributes ) {
+			value = def.attributes[ attr ];
 
 
 			// Attribute bound directly to the model.
 			// Attribute bound directly to the model.
 			if ( typeof value == 'function' ) {
 			if ( typeof value == 'function' ) {
@@ -132,12 +164,10 @@ export default class Template {
 	 * @param {HTMLElement} el Element which is rendered.
 	 * @param {HTMLElement} el Element which is rendered.
 	 */
 	 */
 	_renderElementChildren( def, el ) {
 	_renderElementChildren( def, el ) {
-		let child;
-
 		if ( def.children ) {
 		if ( def.children ) {
-			for ( child of def.children ) {
-				el.appendChild( this._renderElement( child ) );
-			}
+			def.children.map( childDef => {
+				el.appendChild( this._renderNode( childDef ) );
+			} );
 		}
 		}
 	}
 	}
 
 
@@ -204,15 +234,17 @@ function getAttributeUpdater( attr ) {
  *			children: [
  *			children: [
  *				{
  *				{
  *					tag: 'span',
  *					tag: 'span',
- *					attrs: { ... },
- *					on: { ... }
+ *					attributes: { ... },
+ *					children: [ ... ],
+ *					...
  *				},
  *				},
  *				{
  *				{
- *					...
+ *					text: 'abc'
  *				},
  *				},
+ *				'def',
  *				...
  *				...
  *			],
  *			],
- *			attrs: {
+ *			attributes: {
  *				'class': [ 'a', 'b' ],
  *				'class': [ 'a', 'b' ],
  *				id: 'c',
  *				id: 'c',
  *				style: callback,
  *				style: callback,
@@ -224,15 +256,14 @@ function getAttributeUpdater( attr ) {
  *				'event3@selector': 'd',
  *				'event3@selector': 'd',
  *				'event4@selector': [ 'e', 'f', callback ],
  *				'event4@selector': [ 'e', 'f', callback ],
  *				...
  *				...
- *			},
- *			text: 'abc'
+ *			}
  *		}
  *		}
  *
  *
  * @typedef TemplateDefinition
  * @typedef TemplateDefinition
  * @type Object
  * @type Object
  * @property {String} tag
  * @property {String} tag
  * @property {Array} [children]
  * @property {Array} [children]
- * @property {Object} [attrs]
+ * @property {Object} [attributes]
  * @property {String} [text]
  * @property {String} [text]
  * @property {Object} [on]
  * @property {Object} [on]
  */
  */

+ 67 - 22
packages/ckeditor5-ui/tests/ui/template.js

@@ -10,6 +10,7 @@
 
 
 import testUtils from '/tests/_utils/utils.js';
 import testUtils from '/tests/_utils/utils.js';
 import Template from '/ckeditor5/core/ui/template.js';
 import Template from '/ckeditor5/core/ui/template.js';
+import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
 
 testUtils.createSinonSandbox();
 testUtils.createSinonSandbox();
 
 
@@ -20,23 +21,32 @@ describe( 'Template', () => {
 				tag: 'p'
 				tag: 'p'
 			};
 			};
 
 
-			expect( new Template( def ).def ).to.equal( def );
+			expect( new Template( def ).definition ).to.equal( def );
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'render', () => {
 	describe( 'render', () => {
-		it( 'returns null when no definition', () => {
-			expect( new Template().render() ).to.be.null;
+		it( 'throws when wrong template definition', () => {
+			expect( () => {
+				new Template( {} ).render();
+			} ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
+
+			expect( () => {
+				new Template( {
+					tag: 'p',
+					text: 'foo'
+				} ).render();
+			} ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
 		} );
 		} );
 
 
-		it( 'creates an element', () => {
+		it( 'creates a HTMLElement', () => {
 			let el = new Template( {
 			let el = new Template( {
 				tag: 'p',
 				tag: 'p',
-				attrs: {
+				attributes: {
 					'class': [ 'a', 'b' ],
 					'class': [ 'a', 'b' ],
 					x: 'bar'
 					x: 'bar'
 				},
 				},
-				text: 'foo'
+				children: [ 'foo' ]
 			} ).render();
 			} ).render();
 
 
 			expect( el ).to.be.instanceof( HTMLElement );
 			expect( el ).to.be.instanceof( HTMLElement );
@@ -44,24 +54,24 @@ describe( 'Template', () => {
 			expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
 			expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
 		} );
 		} );
 
 
-		it( 'creates element\'s children', () => {
+		it( 'creates HTMLElement\'s children', () => {
 			let el = new Template( {
 			let el = new Template( {
 				tag: 'p',
 				tag: 'p',
-				attrs: {
+				attributes: {
 					a: 'A'
 					a: 'A'
 				},
 				},
 				children: [
 				children: [
 					{
 					{
 						tag: 'b',
 						tag: 'b',
-						text: 'B'
+						children: [ 'B' ]
 					},
 					},
 					{
 					{
 						tag: 'i',
 						tag: 'i',
-						text: 'C',
 						children: [
 						children: [
+							'C',
 							{
 							{
 								tag: 'b',
 								tag: 'b',
-								text: 'D'
+								children: [ 'D' ]
 							}
 							}
 						]
 						]
 					}
 					}
@@ -71,6 +81,35 @@ describe( 'Template', () => {
 			expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
 			expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
 		} );
 		} );
 
 
+		it( 'creates a Text node', () => {
+			let node = new Template( { text: 'foo' } ).render();
+
+			expect( node.nodeType ).to.be.equal( 3 );
+			expect( node.textContent ).to.be.equal( 'foo' );
+		} );
+
+		it( 'creates a child Text Node (different syntaxes)', () => {
+			let el = new Template( {
+				tag: 'p',
+				children: [
+					'foo',
+					{ text: 'bar' }
+				]
+			} ).render();
+
+			expect( el.outerHTML ).to.be.equal( '<p>foobar</p>' );
+		} );
+
+		it( 'creates multiple child Text Nodes', () => {
+			let el = new Template( {
+				tag: 'p',
+				children: [ 'a', 'b', { text: 'c' }, 'd' ]
+			} ).render();
+
+			expect( el.childNodes ).to.have.length( 4 );
+			expect( el.outerHTML ).to.be.equal( '<p>abcd</p>' );
+		} );
+
 		describe( 'callback', () => {
 		describe( 'callback', () => {
 			it( 'works for attributes', () => {
 			it( 'works for attributes', () => {
 				let spy1 = testUtils.sinon.spy();
 				let spy1 = testUtils.sinon.spy();
@@ -78,13 +117,13 @@ describe( 'Template', () => {
 
 
 				let el = new Template( {
 				let el = new Template( {
 					tag: 'p',
 					tag: 'p',
-					attrs: {
+					attributes: {
 						'class': spy1
 						'class': spy1
 					},
 					},
 					children: [
 					children: [
 						{
 						{
 							tag: 'span',
 							tag: 'span',
-							attrs: {
+							attributes: {
 								id: spy2
 								id: spy2
 							}
 							}
 						}
 						}
@@ -106,23 +145,29 @@ describe( 'Template', () => {
 
 
 				let el = new Template( {
 				let el = new Template( {
 					tag: 'p',
 					tag: 'p',
-					text: spy1,
 					children: [
 					children: [
+						{
+							text: spy1
+						},
 						{
 						{
 							tag: 'span',
 							tag: 'span',
-							text: spy2
+							children: [
+								{
+									text: spy2
+								}
+							]
 						}
 						}
 					]
 					]
 				} ).render();
 				} ).render();
 
 
-				sinon.assert.calledWithExactly( spy1, el, sinon.match.func );
-				sinon.assert.calledWithExactly( spy2, el.firstChild, sinon.match.func );
+				sinon.assert.calledWithExactly( spy1, el.firstChild, sinon.match.func );
+				sinon.assert.calledWithExactly( spy2, el.lastChild.firstChild, sinon.match.func );
 
 
-				spy2.firstCall.args[ 1 ]( el.firstChild, 'bar' );
+				spy2.firstCall.args[ 1 ]( el.lastChild.firstChild, 'bar' );
 				expect( el.outerHTML ).to.be.equal( '<p><span>bar</span></p>' );
 				expect( el.outerHTML ).to.be.equal( '<p><span>bar</span></p>' );
 
 
-				spy1.firstCall.args[ 1 ]( el, 'foo' );
-				expect( el.outerHTML ).to.be.equal( '<p>foo</p>' );
+				spy1.firstCall.args[ 1 ]( el.firstChild, 'foo' );
+				expect( el.outerHTML ).to.be.equal( '<p>foo<span>bar</span></p>' );
 			} );
 			} );
 
 
 			it( 'works for "on" property', () => {
 			it( 'works for "on" property', () => {
@@ -164,13 +209,13 @@ describe( 'Template', () => {
 					children: [
 					children: [
 						{
 						{
 							tag: 'span',
 							tag: 'span',
-							attrs: {
+							attributes: {
 								'id': 'x'
 								'id': 'x'
 							}
 							}
 						},
 						},
 						{
 						{
 							tag: 'span',
 							tag: 'span',
-							attrs: {
+							attributes: {
 								'class': 'y'
 								'class': 'y'
 							},
 							},
 							on: {
 							on: {

+ 22 - 15
packages/ckeditor5-ui/tests/ui/view.js

@@ -243,10 +243,10 @@ describe( 'View', () => {
 			setTestViewClass( function() {
 			setTestViewClass( function() {
 				return {
 				return {
 					tag: 'p',
 					tag: 'p',
-					attrs: {
+					attributes: {
 						'class': this.bindToAttribute( 'foo' )
 						'class': this.bindToAttribute( 'foo' )
 					},
 					},
-					text: 'abc'
+					children: [ 'abc' ]
 				};
 				};
 			} );
 			} );
 
 
@@ -263,12 +263,14 @@ describe( 'View', () => {
 				return {
 				return {
 					tag: 'p',
 					tag: 'p',
 					children: [
 					children: [
+						{
+							text: this.bindToAttribute( 'foo' )
+						},
 						{
 						{
 							tag: 'b',
 							tag: 'b',
-							text: 'baz'
+							children: [ 'baz' ]
 						}
 						}
-					],
-					text: this.bindToAttribute( 'foo' )
+					]
 				};
 				};
 			} );
 			} );
 
 
@@ -276,9 +278,8 @@ describe( 'View', () => {
 
 
 			expect( view.element.outerHTML ).to.be.equal( '<p>bar<b>baz</b></p>' );
 			expect( view.element.outerHTML ).to.be.equal( '<p>bar<b>baz</b></p>' );
 
 
-			// TODO: A solution to avoid nuking the children?
 			view.model.foo = 'qux';
 			view.model.foo = 'qux';
-			expect( view.element.outerHTML ).to.be.equal( '<p>qux</p>' );
+			expect( view.element.outerHTML ).to.be.equal( '<p>qux<b>baz</b></p>' );
 		} );
 		} );
 
 
 		it( 'allows binding to the model with value processing', () => {
 		it( 'allows binding to the model with value processing', () => {
@@ -288,10 +289,12 @@ describe( 'View', () => {
 			setTestViewClass( function() {
 			setTestViewClass( function() {
 				return {
 				return {
 					tag: 'p',
 					tag: 'p',
-					attrs: {
+					attributes: {
 						'class': this.bindToAttribute( 'foo', callback )
 						'class': this.bindToAttribute( 'foo', callback )
 					},
 					},
-					text: this.bindToAttribute( 'foo', callback )
+					children: [
+						{ text: this.bindToAttribute( 'foo', callback ) }
+					]
 				};
 				};
 			} );
 			} );
 
 
@@ -306,7 +309,7 @@ describe( 'View', () => {
 			setTestViewClass( function() {
 			setTestViewClass( function() {
 				return {
 				return {
 					tag: 'p',
 					tag: 'p',
-					attrs: {
+					attributes: {
 						'class': this.bindToAttribute( 'foo', ( el, value ) => {
 						'class': this.bindToAttribute( 'foo', ( el, value ) => {
 							el.innerHTML = value;
 							el.innerHTML = value;
 
 
@@ -315,12 +318,14 @@ describe( 'View', () => {
 							}
 							}
 						} )
 						} )
 					},
 					},
-					text: 'bar'
+					children: [ 'bar' ]
 				};
 				};
 			} );
 			} );
 
 
 			setTestViewInstance( { foo: 'moo' } );
 			setTestViewInstance( { foo: 'moo' } );
-			expect( view.element.outerHTML ).to.be.equal( '<p>moo</p>' );
+			// Note: First the attribute binding sets innerHTML to 'moo',
+			// then 'bar' TextNode child is added.
+			expect( view.element.outerHTML ).to.be.equal( '<p>moobar</p>' );
 
 
 			view.model.foo = 'changed';
 			view.model.foo = 'changed';
 			expect( view.element.outerHTML ).to.be.equal( '<p class="changed">changed</p>' );
 			expect( view.element.outerHTML ).to.be.equal( '<p class="changed">changed</p>' );
@@ -391,7 +396,7 @@ describe( 'View', () => {
 					children: [
 					children: [
 						{
 						{
 							tag: 'span',
 							tag: 'span',
-							attrs: {
+							attributes: {
 								'class': 'y',
 								'class': 'y',
 							},
 							},
 							on: {
 							on: {
@@ -403,7 +408,7 @@ describe( 'View', () => {
 							children: [
 							children: [
 								{
 								{
 									tag: 'span',
 									tag: 'span',
-									attrs: {
+									attributes: {
 										'class': 'y',
 										'class': 'y',
 									}
 									}
 								}
 								}
@@ -593,7 +598,9 @@ describe( 'View', () => {
 			setTestViewClass( function() {
 			setTestViewClass( function() {
 				return {
 				return {
 					tag: 'p',
 					tag: 'p',
-					text: this.bindToAttribute( 'foo' )
+					children: [
+						{ text: this.bindToAttribute( 'foo' ) }
+					]
 				};
 				};
 			} );
 			} );