| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /* global document */
- 'use strict';
- import CKEditorError from '../ckeditorerror.js';
- /**
- * Basic Template class.
- *
- * @class Template
- */
- export default class Template {
- /**
- * Creates an instance of the {@link Template} class.
- *
- * @param {TemplateDefinition} definition The definition of the template.
- * @constructor
- */
- constructor( def ) {
- /**
- * Definition of this template.
- *
- * @property {TemplateDefinition}
- */
- this.definition = def;
- }
- /**
- * Renders DOM Node using {@link #definition}.
- *
- * See: {@link #apply}.
- *
- * @returns {HTMLElement}
- */
- render() {
- return this._renderNode( this.definition, null, true );
- }
- /**
- * Applies template {@link #def} to existing DOM tree.
- *
- * **Note:** No new DOM nodes (elements, text nodes) will be created.
- *
- * See: {@link #render}, {@link View#applyTemplateToElement}.
- *
- * @param {Node} element Root element for template to apply.
- */
- apply( node ) {
- if ( !node ) {
- /**
- * No DOM Node specified.
- *
- * @error ui-template-wrong-syntax
- */
- throw new CKEditorError( 'ui-template-wrong-node' );
- }
- return this._renderNode( this.definition, node );
- }
- /**
- * Renders a DOM Node from definition.
- *
- * @protected
- * @param {TemplateDefinition} def Definition of a Node.
- * @param {Node} applyNode If specified, template `def` will be applied to existing DOM Node.
- * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
- * @returns {HTMLElement} A rendered Node.
- */
- _renderNode( def, applyNode, intoFragment ) {
- const isText = def.text || typeof def == 'string';
- let isInvalid;
- if ( applyNode ) {
- // When applying, a definition cannot have "tag" and "text" at the same time.
- isInvalid = def.tag && isText;
- } else {
- // When rendering, a definition must have either "tag" or "text": XOR( def.tag, isText ).
- isInvalid = def.tag ? isText : !isText;
- }
- if ( isInvalid ) {
- /**
- * Node definition cannot have "tag" and "text" properties at the same time.
- * Node definition must have either "tag" or "text" when rendering new Node.
- *
- * @error ui-template-wrong-syntax
- */
- throw new CKEditorError( 'ui-template-wrong-syntax' );
- }
- return isText ?
- this._renderText( def, applyNode ) : this._renderElement( def, applyNode, intoFragment );
- }
- /**
- * Renders an HTMLElement from TemplateDefinition.
- *
- * @protected
- * @param {TemplateDefinition} def Definition of an element.
- * @param {HTMLElement} applyElement If specified, template `def` will be applied to existing HTMLElement.
- * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
- * @returns {HTMLElement} A rendered element.
- */
- _renderElement( def, applyElement, intoFragment ) {
- const el = applyElement || document.createElement( def.tag );
- this._renderElementAttributes( def, el );
- // Invoke children recursively.
- if ( intoFragment ) {
- const docFragment = document.createDocumentFragment();
- this._renderElementChildren( def, docFragment );
- el.appendChild( docFragment );
- } else {
- this._renderElementChildren( def, el, !!applyElement );
- }
- // Activate DOM bindings for event listeners.
- this._activateElementListenerAttachers( def, el );
- return el;
- }
- /**
- * Renders a Text from TemplateDefinition or String.
- *
- * @protected
- * @param {TemplateDefinition|String} def Definition of Text or its value.
- * @param {HTMLElement} applyText If specified, template `def` will be applied to existing Text Node.
- * @returns {Text} A rendered Text.
- */
- _renderText( defOrText, applyText ) {
- const textNode = applyText || document.createTextNode( '' );
- // Check if there's a binder available for this Text Node.
- const binder = defOrText._modelBinders && defOrText._modelBinders.text;
- // Activate binder if one. Cases:
- // { text: bind.to( ... ) }
- // { text: [ 'foo', bind.to( ... ), ... ] }
- if ( binder ) {
- binder( textNode, getTextNodeUpdater( textNode ) );
- }
- // Simply set text. Cases:
- // { text: [ 'all', 'are', 'static' ] }
- // { text: 'foo' }
- // 'foo'
- else {
- textNode.textContent = defOrText.text || defOrText;
- }
- return textNode;
- }
- /**
- * Renders element attributes from definition.
- *
- * @protected
- * @param {TemplateDefinition} def Definition of an element.
- * @param {HTMLElement} el Element which is rendered.
- */
- _renderElementAttributes( def, el ) {
- const attributes = def.attributes;
- const binders = def._modelBinders && def._modelBinders.attributes;
- let binder, attrName, attrValue;
- if ( !attributes ) {
- return;
- }
- for ( attrName in attributes ) {
- // Check if there's a binder available for this attribute.
- binder = binders && binders[ attrName ];
- // Activate binder if one. Cases:
- // { class: [ 'bar', bind.to( ... ), 'baz' ] }
- // { class: bind.to( ... ) }
- if ( binder ) {
- binder( el, getElementAttributeUpdater( el, attrName ) );
- }
- // Otherwise simply set the attribute.
- // { class: [ 'all', 'are', 'static' ] }
- // { class: 'foo' }
- else {
- attrValue = attributes[ attrName ];
- // Attribute can be an array. Merge array elements:
- if ( Array.isArray( attrValue ) ) {
- attrValue = attrValue.reduce( function binderValueReducer( prev, cur ) {
- return prev === '' ? `${cur}` : `${prev} ${cur}`;
- } );
- }
- el.setAttribute( attrName, attrValue );
- }
- }
- }
- /**
- * Recursively renders element children from definition by
- * calling {@link #_renderElement}.
- *
- * @protected
- * @param {TemplateDefinition} def Definition of an element.
- * @param {HTMLElement} el Element which is rendered.
- * @param {Boolean} isApply Traverse existing DOM structure only, don't modify DOM.
- */
- _renderElementChildren( def, el, isApply ) {
- if ( def.children ) {
- def.children.forEach( ( childDef, index ) => {
- if ( isApply ) {
- this._renderNode( childDef, el.childNodes[ index ] );
- } else {
- el.appendChild( this._renderNode( childDef ) );
- }
- } );
- }
- }
- /**
- * Activates element `on` listeners passed in element definition.
- *
- * @protected
- * @param {TemplateDefinition} def Definition of an element.
- * @param {HTMLElement} el Element which is rendered.
- */
- _activateElementListenerAttachers( def, el ) {
- if ( !def.on ) {
- return;
- }
- const attachers = def.on._listenerAttachers;
- Object.keys( attachers )
- .map( name => [ name, ...name.split( '@' ) ] )
- .forEach( split => {
- // TODO: ES6 destructuring.
- const key = split[ 0 ];
- const evtName = split[ 1 ];
- const evtSelector = split[ 2 ] || null;
- if ( Array.isArray( attachers[ key ] ) ) {
- attachers[ key ].forEach( i => i( el, evtName, evtSelector ) );
- } else {
- attachers[ key ]( el, evtName, evtSelector );
- }
- } );
- }
- }
- /**
- * Returns an object consisting of `set` and `remove` functions, which
- * can be used in the context of DOM Node to set or reset `textContent`.
- * See {@link View#_getModelBinder}.
- *
- * @private
- * @param {Node} node DOM Node to be modified.
- * @returns {Object}
- */
- function getTextNodeUpdater( node ) {
- return {
- set( value ) {
- node.textContent = value;
- },
- remove() {
- node.textContent = '';
- }
- };
- }
- /**
- * Returns an object consisting of `set` and `remove` functions, which
- * can be used in the context of DOM Node to set or reset an attribute.
- * See {@link View#_getModelBinder}.
- *
- * @private
- * @param {Node} node DOM Node to be modified.
- * @param {String} attrName Name of the attribute to be modified.
- * @returns {Object}
- */
- function getElementAttributeUpdater( el, attrName ) {
- return {
- set( value ) {
- el.setAttribute( attrName, value );
- },
- remove() {
- el.removeAttribute( attrName );
- }
- };
- }
- /**
- * Definition of {@link Template}.
- * See: {@link TemplateValueSchema}.
- *
- * {
- * tag: 'p',
- * children: [
- * {
- * tag: 'span',
- * attributes: { ... },
- * children: [ ... ],
- * ...
- * },
- * {
- * text: 'static–text'
- * },
- * 'also-static–text',
- * ...
- * ],
- * attributes: {
- * 'class': [ 'class-a', 'class-b' ],
- * id: 'element-id',
- * style: callback,
- * ...
- * },
- * on: {
- * 'click': 'clicked'
- * 'mouseup': [ 'view-event-a', 'view-event-b', callback ],
- * 'keyup@selector': 'view-event',
- * 'focus@selector': [ 'view-event-a', 'view-event-b', callback ],
- * ...
- * }
- * }
- *
- * @typedef TemplateDefinition
- * @type Object
- * @property {String} tag
- * @property {Array} [children]
- * @property {Object} [attributes]
- * @property {String} [text]
- * @property {Object} [on]
- * @property {Object} _modelBinders
- */
- /**
- * Describes a value of HTMLElement attribute or `textContent`.
- * See: {@link TemplateDefinition}.
- *
- * {
- * tag: 'p',
- * attributes: {
- * // Plain String schema.
- * class: 'class-foo'
- *
- * // Object schema, a Model binding.
- * class: { model: m, attribute: 'foo', callback... }
- *
- * // Array schema, combines the above.
- * class: [ 'foo', { model: m, attribute: 'bar' }, 'baz' ]
- * }
- * }
- *
- * @typedef TemplateValueSchema
- * @type {Object|String|Array}
- */
|