| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module ui/template
- */
- /* global document */
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
- import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
- import View from './view';
- import ViewCollection from './viewcollection';
- import cloneDeepWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeepWith';
- import isObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isObject';
- import isDomNode from '@ckeditor/ckeditor5-utils/src/dom/isdomnode';
- import log from '@ckeditor/ckeditor5-utils/src/log';
- const xhtmlNs = 'http://www.w3.org/1999/xhtml';
- /**
- * A basic Template class. It renders a DOM HTML element or text from a
- * {@link module:ui/template~TemplateDefinition definition} and supports element attributes, children,
- * bindings to {@link module:utils/observablemixin~Observable observables} and DOM event propagation.
- *
- * A simple template can look like this:
- *
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * class: 'foo',
- * style: {
- * backgroundColor: 'yellow'
- * }
- * },
- * on: {
- * click: bind.to( 'clicked' )
- * }
- * children: [
- * 'A paragraph.'
- * ]
- * } ).render();
- *
- * and it will render the following HTML element:
- *
- * <p class="foo" style="background-color: yellow;">A paragraph.</p>
- *
- * Additionally, the `observable` will always fire `clicked` upon clicking `<p>` in the DOM.
- *
- * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex
- * template definitions.
- *
- * @mixes module:utils/emittermixin~EmitterMixin
- */
- export default class Template {
- /**
- * Creates an instance of the {@link ~Template} class.
- *
- * @param {module:ui/template~TemplateDefinition} def The definition of the template.
- */
- constructor( def ) {
- Object.assign( this, normalize( clone( def ) ) );
- /**
- * Indicates whether this particular Template instance has been
- * {@link #render rendered}.
- *
- * @readonly
- * @protected
- * @member {Boolean}
- */
- this._isRendered = false;
- /**
- * The tag (`tagName`) of this template, e.g. `div`. It also indicates that the template
- * renders to an HTML element.
- *
- * @member {String} #tag
- */
- /**
- * The text of the template. It also indicates that the template renders to a DOM text node.
- *
- * @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
- */
- /**
- * The attributes of the template, e.g. `{ id: [ 'ck-id' ] }`, corresponding with
- * the attributes of an HTML element.
- *
- * **Note**: This property only makes sense when {@link #tag} is defined.
- *
- * @member {Object} #attributes
- */
- /**
- * The children of the template. They can be either:
- * * independent instances of {@link ~Template} (sub–templates),
- * * native DOM Nodes.
- *
- * **Note**: This property only makes sense when {@link #tag} is defined.
- *
- * @member {Array.<module:ui/template~Template|Node>} #children
- */
- /**
- * The DOM event listeners of the template.
- *
- * @member {Object} #eventListeners
- */
- /**
- * The data used by the {@link #revert} method to restore a node to its original state.
- *
- * See: {@link #apply}.
- *
- * @readonly
- * @protected
- * @member {module:ui/template~RenderData}
- */
- this._revertData = null;
- }
- /**
- * Renders a DOM Node (an HTML element or text) out of the template.
- *
- * const domNode = new Template( { ... } ).render();
- *
- * See: {@link #apply}.
- *
- * @returns {HTMLElement|Text}
- */
- render() {
- const node = this._renderNode( {
- intoFragment: true
- } );
- this._isRendered = true;
- return node;
- }
- /**
- * Applies the template to an existing DOM Node, either HTML element or text.
- *
- * **Note:** No new DOM nodes will be created. Applying extends
- * {@link module:ui/template~TemplateDefinition attributes} and
- * {@link module:ui/template~TemplateDefinition event listeners} only.
- *
- * **Note:** Existing `class` and `style` attributes are extended when a template
- * is applied to an HTML element, while other attributes and `textContent` are overridden.
- *
- * **Note:** The process of applying a template can be easily reverted using the
- * {@link module:ui/template~Template#revert} method.
- *
- * const element = document.createElement( 'div' );
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * attrs: {
- * id: 'first-div',
- * class: bind.to( 'divClass' )
- * },
- * on: {
- * click: bind( 'elementClicked' ) // Will be fired by the observable.
- * }
- * children: [
- * 'Div text.'
- * ]
- * } ).apply( element );
- *
- * element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
- *
- * @see module:ui/template~Template#render
- * @see module:ui/template~Template#revert
- * @param {Node} node Root node for the template to apply.
- */
- apply( node ) {
- this._revertData = getEmptyRevertData();
- this._renderNode( {
- node,
- isApplying: true,
- revertData: this._revertData
- } );
- return node;
- }
- /**
- * Reverts a template {@link module:ui/template~Template#apply applied} to a DOM node.
- *
- * @param {Node} node The root node for the template to revert. In most of the cases, it is the
- * same node used by {@link module:ui/template~Template#apply}.
- */
- revert( node ) {
- if ( !this._revertData ) {
- /**
- * Attempting to revert a template which has not been applied yet.
- *
- * @error ui-template-revert-not-applied
- */
- throw new CKEditorError( 'ui-template-revert-not-applied: Attempting to revert a template which has not been applied yet.' );
- }
- this._revertTemplateFromNode( node, this._revertData );
- }
- /**
- * Returns an iterator which traverses the template in search of {@link module:ui/view~View}
- * instances and returns them one by one.
- *
- * const viewFoo = new View();
- * const viewBar = new View();
- * const viewBaz = new View();
- * const template = new Template( {
- * tag: 'div',
- * children: [
- * viewFoo,
- * {
- * tag: 'div',
- * children: [
- * viewBar
- * ]
- * },
- * viewBaz
- * ]
- * } );
- *
- * // Logs: viewFoo, viewBar, viewBaz
- * for ( const view of template.getViews() ) {
- * console.log( view );
- * }
- *
- * @returns {Iterable.<module:ui/view~View>}
- */
- * getViews() {
- function* search( def ) {
- if ( def.children ) {
- for ( const child of def.children ) {
- if ( isView( child ) ) {
- yield child;
- } else if ( isTemplate( child ) ) {
- yield* search( child );
- }
- }
- }
- }
- yield* search( this );
- }
- /**
- * An entry point to the interface which binds DOM nodes to
- * {@link module:utils/observablemixin~Observable observables}.
- * There are two types of bindings:
- *
- * * HTML element attributes or text `textContent` synchronized with attributes of an
- * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}
- * and {@link module:ui/template~BindChain#if}.
- *
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * attrs: {
- * // Binds the element "class" attribute to observable#classAttribute.
- * class: bind.to( 'classAttribute' )
- * }
- * } ).render();
- *
- * * DOM events fired on HTML element propagated through
- * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}.
- *
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * on: {
- * // Will be fired by the observable.
- * click: bind( 'elementClicked' )
- * }
- * } ).render();
- *
- * Also see {@link module:ui/view~View#bindTemplate}.
- *
- * @param {module:utils/observablemixin~Observable} observable An observable which provides boundable attributes.
- * @param {module:utils/emittermixin~Emitter} emitter An emitter that listens to observable attribute
- * changes or DOM Events (depending on the kind of the binding). Usually, a {@link module:ui/view~View} instance.
- * @returns {module:ui/template~BindChain}
- */
- static bind( observable, emitter ) {
- return {
- to( eventNameOrFunctionOrAttribute, callback ) {
- return new TemplateToBinding( {
- eventNameOrFunction: eventNameOrFunctionOrAttribute,
- attribute: eventNameOrFunctionOrAttribute,
- observable, emitter, callback
- } );
- },
- if( attribute, valueIfTrue, callback ) {
- return new TemplateIfBinding( {
- observable, emitter, attribute, valueIfTrue, callback
- } );
- }
- };
- }
- /**
- * Extends an existing {@link module:ui/template~Template} instance with some additional content
- * from another {@link module:ui/template~TemplateDefinition}.
- *
- * const bind = Template.bind( observable, emitter );
- *
- * const template = new Template( {
- * tag: 'p',
- * attributes: {
- * class: 'a',
- * data-x: bind.to( 'foo' )
- * },
- * children: [
- * {
- * tag: 'span',
- * attributes: {
- * class: 'b'
- * },
- * children: [
- * 'Span'
- * ]
- * }
- * ]
- * } );
- *
- * // Instance-level extension.
- * Template.extend( template, {
- * attributes: {
- * class: 'b',
- * data-x: bind.to( 'bar' )
- * },
- * children: [
- * {
- * attributes: {
- * class: 'c'
- * }
- * }
- * ]
- * } );
- *
- * // Child extension.
- * Template.extend( template.children[ 0 ], {
- * attributes: {
- * class: 'd'
- * }
- * } );
- *
- * the `outerHTML` of `template.render()` is:
- *
- * <p class="a b" data-x="{ observable.foo } { observable.bar }">
- * <span class="b c d">Span</span>
- * </p>
- *
- * @param {module:ui/template~Template} template An existing template instance to be extended.
- * @param {module:ui/template~TemplateDefinition} def Additional definition to be applied to a template.
- */
- static extend( template, def ) {
- if ( template._isRendered ) {
- /**
- * Extending a template after rendering may not work as expected. To make sure
- * the {@link module:ui/template~Template.extend extending} works for an element,
- * make sure it happens before {@link #render} is called.
- *
- * @error template-extend-render
- */
- log.warn( 'template-extend-render: Attempting to extend a template which has already been rendered.' );
- }
- extendTemplate( template, normalize( clone( def ) ) );
- }
- /**
- * Renders a DOM Node (either an HTML element or text) out of the template.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderNode( data ) {
- let isInvalid;
- if ( data.node ) {
- // When applying, a definition cannot have "tag" and "text" at the same time.
- isInvalid = this.tag && this.text;
- } else {
- // When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
- isInvalid = this.tag ? this.text : !this.text;
- }
- if ( isInvalid ) {
- /**
- * Node definition cannot have the "tag" and "text" properties at the same time.
- * Node definition must have either "tag" or "text" when rendering a new Node.
- *
- * @error ui-template-wrong-syntax
- */
- throw new CKEditorError(
- 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering a new Node.'
- );
- }
- if ( this.text ) {
- return this._renderText( data );
- } else {
- return this._renderElement( data );
- }
- }
- /**
- * Renders an HTML element out of the template.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderElement( data ) {
- let node = data.node;
- if ( !node ) {
- node = data.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
- }
- this._renderAttributes( data );
- this._renderElementChildren( data );
- this._setUpListeners( data );
- return node;
- }
- /**
- * Renders a text node out of {@link module:ui/template~Template#text}.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderText( data ) {
- let node = data.node;
- // Save the original textContent to revert it in #revert().
- if ( node ) {
- data.revertData.text = node.textContent;
- } else {
- node = data.node = document.createTextNode( '' );
- }
- // Check if this Text Node is bound to Observable. Cases:
- //
- // text: [ Template.bind( ... ).to( ... ) ]
- //
- // text: [
- // 'foo',
- // Template.bind( ... ).to( ... ),
- // ...
- // ]
- //
- if ( hasTemplateBinding( this.text ) ) {
- this._bindToObservable( {
- schema: this.text,
- updater: getTextUpdater( node ),
- data
- } );
- }
- // Simply set text. Cases:
- //
- // text: [ 'all', 'are', 'static' ]
- //
- // text: [ 'foo' ]
- //
- else {
- node.textContent = this.text.join( '' );
- }
- return node;
- }
- /**
- * Renders HTML element attributes out of {@link module:ui/template~Template#attributes}.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderAttributes( data ) {
- let attrName, attrValue, domAttrValue, attrNs;
- if ( !this.attributes ) {
- return;
- }
- const node = data.node;
- const revertData = data.revertData;
- for ( attrName in this.attributes ) {
- // Current attribute value in DOM.
- domAttrValue = node.getAttribute( attrName );
- // The value to be set.
- attrValue = this.attributes[ attrName ];
- // Save revert data.
- if ( revertData ) {
- revertData.attributes[ attrName ] = domAttrValue;
- }
- // Detect custom namespace:
- //
- // class: {
- // ns: 'abc',
- // value: Template.bind( ... ).to( ... )
- // }
- //
- attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;
- // Activate binding if one is found. Cases:
- //
- // class: [
- // Template.bind( ... ).to( ... )
- // ]
- //
- // class: [
- // 'bar',
- // Template.bind( ... ).to( ... ),
- // 'baz'
- // ]
- //
- // class: {
- // ns: 'abc',
- // value: Template.bind( ... ).to( ... )
- // }
- //
- if ( hasTemplateBinding( attrValue ) ) {
- // Normalize attributes with additional data like namespace:
- //
- // class: {
- // ns: 'abc',
- // value: [ ... ]
- // }
- //
- const valueToBind = attrNs ? attrValue[ 0 ].value : attrValue;
- // Extend the original value of attributes like "style" and "class",
- // don't override them.
- if ( revertData && shouldExtend( attrName ) ) {
- valueToBind.unshift( domAttrValue );
- }
- this._bindToObservable( {
- schema: valueToBind,
- updater: getAttributeUpdater( node, attrName, attrNs ),
- data
- } );
- }
- // Style attribute could be an Object so it needs to be parsed in a specific way.
- //
- // style: {
- // width: '100px',
- // height: Template.bind( ... ).to( ... )
- // }
- //
- else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
- this._renderStyleAttribute( attrValue[ 0 ], data );
- }
- // Otherwise simply set the static attribute:
- //
- // class: [ 'foo' ]
- //
- // class: [ 'all', 'are', 'static' ]
- //
- // class: [
- // {
- // ns: 'abc',
- // value: [ 'foo' ]
- // }
- // ]
- //
- else {
- // Extend the original value of attributes like "style" and "class",
- // don't override them.
- if ( revertData && domAttrValue && shouldExtend( attrName ) ) {
- attrValue.unshift( domAttrValue );
- }
- attrValue = attrValue
- // Retrieve "values" from:
- //
- // class: [
- // {
- // ns: 'abc',
- // value: [ ... ]
- // }
- // ]
- //
- .map( val => val ? ( val.value || val ) : val )
- // Flatten the array.
- .reduce( ( prev, next ) => prev.concat( next ), [] )
- // Convert into string.
- .reduce( arrayValueReducer, '' );
- if ( !isFalsy( attrValue ) ) {
- node.setAttributeNS( attrNs, attrName, attrValue );
- }
- }
- }
- }
- /**
- * Renders the `style` attribute of an HTML element based on
- * {@link module:ui/template~Template#attributes}.
- *
- * A style attribute is an {Object} with static values:
- *
- * attributes: {
- * style: {
- * color: 'red'
- * }
- * }
- *
- * or values bound to {@link module:ui/model~Model} properties:
- *
- * attributes: {
- * style: {
- * color: bind.to( ... )
- * }
- * }
- *
- * Note: The `style` attribute is rendered without setting the namespace. It does not seem to be
- * needed.
- *
- * @private
- * @param {Object} styles Styles located in `attributes.style` of {@link module:ui/template~TemplateDefinition}.
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderStyleAttribute( styles, data ) {
- const node = data.node;
- for ( const styleName in styles ) {
- const styleValue = styles[ styleName ];
- // Cases:
- //
- // style: {
- // color: bind.to( 'attribute' )
- // }
- //
- if ( hasTemplateBinding( styleValue ) ) {
- this._bindToObservable( {
- schema: [ styleValue ],
- updater: getStyleUpdater( node, styleName ),
- data
- } );
- }
- // Cases:
- //
- // style: {
- // color: 'red'
- // }
- //
- else {
- node.style[ styleName ] = styleValue;
- }
- }
- }
- /**
- * Recursively renders HTML element's children from {@link module:ui/template~Template#children}.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _renderElementChildren( data ) {
- const node = data.node;
- const container = data.intoFragment ? document.createDocumentFragment() : node;
- const isApplying = data.isApplying;
- let childIndex = 0;
- for ( const child of this.children ) {
- if ( isViewCollection( child ) ) {
- if ( !isApplying ) {
- child.setParent( node );
- // Note: ViewCollection renders its children.
- for ( const view of child ) {
- container.appendChild( view.element );
- }
- }
- } else if ( isView( child ) ) {
- if ( !isApplying ) {
- if ( !child.isRendered ) {
- child.render();
- }
- container.appendChild( child.element );
- }
- } else if ( isDomNode( child ) ) {
- container.appendChild( child );
- } else {
- if ( isApplying ) {
- const revertData = data.revertData;
- const childRevertData = getEmptyRevertData();
- revertData.children.push( childRevertData );
- child._renderNode( {
- node: container.childNodes[ childIndex++ ],
- isApplying: true,
- revertData: childRevertData
- } );
- } else {
- container.appendChild( child.render() );
- }
- }
- }
- if ( data.intoFragment ) {
- node.appendChild( container );
- }
- }
- /**
- * Activates `on` event listeners from the {@link module:ui/template~TemplateDefinition}
- * on an HTML element.
- *
- * @protected
- * @param {module:ui/template~RenderData} data Rendering data.
- */
- _setUpListeners( data ) {
- if ( !this.eventListeners ) {
- return;
- }
- for ( const key in this.eventListeners ) {
- const revertBindings = this.eventListeners[ key ].map( schemaItem => {
- const [ domEvtName, domSelector ] = key.split( '@' );
- return schemaItem.activateDomEventListener( domEvtName, domSelector, data );
- } );
- if ( data.revertData ) {
- data.revertData.bindings.push( revertBindings );
- }
- }
- }
- /**
- * For a given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding}
- * activates the binding and sets its initial value.
- *
- * Note: {@link module:ui/template~TemplateValueSchema} can be for HTML element attributes or
- * text node `textContent`.
- *
- * @protected
- * @param {Object} options Binding options.
- * @param {module:ui/template~TemplateValueSchema} options.schema
- * @param {Function} options.updater A function which updates the DOM (like attribute or text).
- * @param {module:ui/template~RenderData} options.data Rendering data.
- */
- _bindToObservable( { schema, updater, data } ) {
- const revertData = data.revertData;
- // Set initial values.
- syncValueSchemaValue( schema, updater, data );
- const revertBindings = schema
- // Filter "falsy" (false, undefined, null, '') value schema components out.
- .filter( item => !isFalsy( item ) )
- // Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
- .filter( item => item.observable )
- // Once only the actual binding are left, let the emitter listen to observable change:attribute event.
- // TODO: Reduce the number of listeners attached as many bindings may listen
- // to the same observable attribute.
- .map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );
- if ( revertData ) {
- revertData.bindings.push( revertBindings );
- }
- }
- /**
- * Reverts {@link module:ui/template~RenderData#revertData template data} from a node to
- * return it to the original state.
- *
- * @protected
- * @param {HTMLElement|Text} node A node to be reverted.
- * @param {module:ui/template~RenderData#revertData} revertData Stores information about
- * what changes have been made by {@link #apply} to the node.
- */
- _revertTemplateFromNode( node, revertData ) {
- for ( const binding of revertData.bindings ) {
- // Each binding may consist of several observable+observable#attribute.
- // like the following has 2:
- //
- // class: [
- // 'x',
- // bind.to( 'foo' ),
- // 'y',
- // bind.to( 'bar' )
- // ]
- //
- for ( const revertBinding of binding ) {
- revertBinding();
- }
- }
- if ( revertData.text ) {
- node.textContent = revertData.text;
- return;
- }
- for ( const attrName in revertData.attributes ) {
- const attrValue = revertData.attributes[ attrName ];
- // When the attribute has **not** been set before #apply().
- if ( attrValue === null ) {
- node.removeAttribute( attrName );
- } else {
- node.setAttribute( attrName, attrValue );
- }
- }
- for ( let i = 0; i < revertData.children.length; ++i ) {
- this._revertTemplateFromNode( node.childNodes[ i ], revertData.children[ i ] );
- }
- }
- }
- mix( Template, EmitterMixin );
- /**
- * Describes a binding created by the {@link module:ui/template~Template.bind} interface.
- *
- * @protected
- */
- export class TemplateBinding {
- /**
- * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
- *
- * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
- */
- constructor( def ) {
- Object.assign( this, def );
- /**
- * An observable instance of the binding. It either:
- *
- * * provides the attribute with the value,
- * * or passes the event when a corresponding DOM event is fired.
- *
- * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
- */
- /**
- * An {@link module:utils/emittermixin~Emitter} used by the binding to:
- *
- * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
- * * or listen to the event in the DOM.
- *
- * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
- */
- /**
- * The name of the {@link module:ui/template~TemplateBinding#observable observed attribute}.
- *
- * @member {String} module:ui/template~TemplateBinding#attribute
- */
- /**
- * A custom function to process the value of the {@link module:ui/template~TemplateBinding#attribute}.
- *
- * @member {Function} [module:ui/template~TemplateBinding#callback]
- */
- }
- /**
- * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
- * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
- *
- * @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
- * @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable}.
- */
- getValue( node ) {
- const value = this.observable[ this.attribute ];
- return this.callback ? this.callback( value, node ) : value;
- }
- /**
- * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
- * value of {@link module:ui/template~TemplateValueSchema}.
- *
- * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in the DOM.
- * @param {Function} updater A DOM updater function used to update the native DOM attribute or text.
- * @param {module:ui/template~RenderData} data Rendering data.
- * @returns {Function} A function to sever the listener binding.
- */
- activateAttributeListener( schema, updater, data ) {
- const callback = () => syncValueSchemaValue( schema, updater, data );
- this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );
- // Allows revert of the listener.
- return () => {
- this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
- };
- }
- }
- /**
- * Describes either:
- *
- * * a binding to an {@link module:utils/observablemixin~Observable},
- * * or a native DOM event binding.
- *
- * It is created by the {@link module:ui/template~BindChain#to} method.
- *
- * @protected
- */
- export class TemplateToBinding extends TemplateBinding {
- /**
- * Activates the listener for the native DOM event, which when fired, is propagated by
- * the {@link module:ui/template~TemplateBinding#emitter}.
- *
- * @param {String} domEvtName The name of the native DOM event.
- * @param {String} domSelector The selector in the DOM to filter delegated events.
- * @param {module:ui/template~RenderData} data Rendering data.
- * @returns {Function} A function to sever the listener binding.
- */
- activateDomEventListener( domEvtName, domSelector, data ) {
- const callback = ( evt, domEvt ) => {
- if ( !domSelector || domEvt.target.matches( domSelector ) ) {
- if ( typeof this.eventNameOrFunction == 'function' ) {
- this.eventNameOrFunction( domEvt );
- } else {
- this.observable.fire( this.eventNameOrFunction, domEvt );
- }
- }
- };
- this.emitter.listenTo( data.node, domEvtName, callback );
- // Allows revert of the listener.
- return () => {
- this.emitter.stopListening( data.node, domEvtName, callback );
- };
- }
- }
- /**
- * Describes a binding to {@link module:utils/observablemixin~ObservableMixin} created by the {@link module:ui/template~BindChain#if}
- * method.
- *
- * @protected
- */
- export class TemplateIfBinding extends TemplateBinding {
- /**
- * @inheritDoc
- */
- getValue( node ) {
- const value = super.getValue( node );
- return isFalsy( value ) ? false : ( this.valueIfTrue || true );
- }
- /**
- * The value of the DOM attribute or text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable} is `true`.
- *
- * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
- */
- }
- // Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
- // {@link module:ui/template~TemplateBinding}.
- //
- // @param {module:ui/template~TemplateValueSchema} schema
- // @returns {Boolean}
- function hasTemplateBinding( schema ) {
- if ( !schema ) {
- return false;
- }
- // Normalize attributes with additional data like namespace:
- //
- // class: {
- // ns: 'abc',
- // value: [ ... ]
- // }
- //
- if ( schema.value ) {
- schema = schema.value;
- }
- if ( Array.isArray( schema ) ) {
- return schema.some( hasTemplateBinding );
- } else if ( schema instanceof TemplateBinding ) {
- return true;
- }
- return false;
- }
- // Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
- // an Array. Each entry of the Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
- // items.
- //
- // @param {module:ui/template~TemplateValueSchema} schema
- // @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
- // @return {Array}
- function getValueSchemaValue( schema, node ) {
- return schema.map( schemaItem => {
- // Process {@link module:ui/template~TemplateBinding} bindings.
- if ( schemaItem instanceof TemplateBinding ) {
- return schemaItem.getValue( node );
- }
- // All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
- return schemaItem;
- } );
- }
- // A function executed each time the bound Observable attribute changes, which updates the DOM with a value
- // constructed from {@link module:ui/template~TemplateValueSchema}.
- //
- // @param {module:ui/template~TemplateValueSchema} schema
- // @param {Function} updater A function which updates the DOM (like attribute or text).
- // @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
- function syncValueSchemaValue( schema, updater, { node } ) {
- let value = getValueSchemaValue( schema, node );
- // Check if schema is a single Template.bind.if, like:
- //
- // class: Template.bind.if( 'foo' )
- //
- if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
- value = value[ 0 ];
- } else {
- value = value.reduce( arrayValueReducer, '' );
- }
- if ( isFalsy( value ) ) {
- updater.remove();
- } else {
- updater.set( value );
- }
- }
- // 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 module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @returns {Object}
- function getTextUpdater( 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 module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @param {String} attrName Name of the attribute to be modified.
- // @param {String} [ns=null] Namespace to use.
- // @returns {Object}
- function getAttributeUpdater( el, attrName, ns ) {
- return {
- set( value ) {
- el.setAttributeNS( ns, attrName, value );
- },
- remove() {
- el.removeAttributeNS( ns, attrName );
- }
- };
- }
- // Returns an object consisting of `set` and `remove` functions, which
- // can be used in the context of CSSStyleDeclaration to set or remove a style.
- // @see module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @param {String} styleName Name of the style to be modified.
- // @returns {Object}
- function getStyleUpdater( el, styleName ) {
- return {
- set( value ) {
- el.style[ styleName ] = value;
- },
- remove() {
- el.style[ styleName ] = null;
- }
- };
- }
- // Clones definition of the template.
- //
- // @param {module:ui/template~TemplateDefinition} def
- // @returns {module:ui/template~TemplateDefinition}
- function clone( def ) {
- const clone = cloneDeepWith( def, value => {
- // Don't clone the `Template.bind`* bindings because of the references to Observable
- // and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
- // cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
- // along with the definition.
- //
- // Don't clone Template instances if provided as a child. They're simply #render()ed
- // and nothing should interfere.
- //
- // Also don't clone View instances if provided as a child of the Template. The template
- // instance will be extracted from the View during the normalization and there's no need
- // to clone it.
- if ( value && ( value instanceof TemplateBinding || isTemplate( value ) || isView( value ) || isViewCollection( value ) ) ) {
- return value;
- }
- } );
- return clone;
- }
- // Normalizes given {@link module:ui/template~TemplateDefinition}.
- //
- // See:
- // * {@link normalizeAttributes}
- // * {@link normalizeListeners}
- // * {@link normalizePlainTextDefinition}
- // * {@link normalizeTextDefinition}
- //
- // @param {module:ui/template~TemplateDefinition} def
- // @returns {module:ui/template~TemplateDefinition} Normalized definition.
- function normalize( def ) {
- if ( typeof def == 'string' ) {
- def = normalizePlainTextDefinition( def );
- } else if ( def.text ) {
- normalizeTextDefinition( def );
- }
- if ( def.on ) {
- def.eventListeners = normalizeListeners( def.on );
- // Template mixes EmitterMixin, so delete #on to avoid collision.
- delete def.on;
- }
- if ( !def.text ) {
- if ( def.attributes ) {
- normalizeAttributes( def.attributes );
- }
- const children = [];
- if ( def.children ) {
- if ( isViewCollection( def.children ) ) {
- children.push( def.children );
- } else {
- for ( const child of def.children ) {
- if ( isTemplate( child ) || isView( child ) || isDomNode( child ) ) {
- children.push( child );
- } else {
- children.push( new Template( child ) );
- }
- }
- }
- }
- def.children = children;
- }
- return def;
- }
- // Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
- //
- // attributes: {
- // a: 'bar',
- // b: {@link module:ui/template~TemplateBinding},
- // c: {
- // value: 'bar'
- // }
- // }
- //
- // becomes
- //
- // attributes: {
- // a: [ 'bar' ],
- // b: [ {@link module:ui/template~TemplateBinding} ],
- // c: {
- // value: [ 'bar' ]
- // }
- // }
- //
- // @param {Object} attrs
- function normalizeAttributes( attrs ) {
- for ( const a in attrs ) {
- if ( attrs[ a ].value ) {
- attrs[ a ].value = [].concat( attrs[ a ].value );
- }
- arrayify( attrs, a );
- }
- }
- // Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
- //
- // on: {
- // a: 'bar',
- // b: {@link module:ui/template~TemplateBinding},
- // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
- // }
- //
- // becomes
- //
- // on: {
- // a: [ 'bar' ],
- // b: [ {@link module:ui/template~TemplateBinding} ],
- // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
- // }
- //
- // @param {Object} listeners
- // @returns {Object} Object containing normalized listeners.
- function normalizeListeners( listeners ) {
- for ( const l in listeners ) {
- arrayify( listeners, l );
- }
- return listeners;
- }
- // Normalizes "string" {@link module:ui/template~TemplateDefinition}.
- //
- // "foo"
- //
- // becomes
- //
- // { text: [ 'foo' ] },
- //
- // @param {String} def
- // @returns {module:ui/template~TemplateDefinition} Normalized template definition.
- function normalizePlainTextDefinition( def ) {
- return {
- text: [ def ]
- };
- }
- // Normalizes text {@link module:ui/template~TemplateDefinition}.
- //
- // children: [
- // { text: 'def' },
- // { text: {@link module:ui/template~TemplateBinding} }
- // ]
- //
- // becomes
- //
- // children: [
- // { text: [ 'def' ] },
- // { text: [ {@link module:ui/template~TemplateBinding} ] }
- // ]
- //
- // @param {module:ui/template~TemplateDefinition} def
- function normalizeTextDefinition( def ) {
- if ( !Array.isArray( def.text ) ) {
- def.text = [ def.text ];
- }
- }
- // Wraps an entry in Object in an Array, if not already one.
- //
- // {
- // x: 'y',
- // a: [ 'b' ]
- // }
- //
- // becomes
- //
- // {
- // x: [ 'y' ],
- // a: [ 'b' ]
- // }
- //
- // @param {Object} obj
- // @param {String} key
- function arrayify( obj, key ) {
- if ( !Array.isArray( obj[ key ] ) ) {
- obj[ key ] = [ obj[ key ] ];
- }
- }
- // A helper which concatenates the value avoiding unwanted
- // leading white spaces.
- //
- // @param {String} prev
- // @param {String} cur
- // @returns {String}
- function arrayValueReducer( prev, cur ) {
- if ( isFalsy( cur ) ) {
- return prev;
- } else if ( isFalsy( prev ) ) {
- return cur;
- } else {
- return `${ prev } ${ cur }`;
- }
- }
- // Extends one object defined in the following format:
- //
- // {
- // key1: [Array1],
- // key2: [Array2],
- // ...
- // keyN: [ArrayN]
- // }
- //
- // with another object of the same data format.
- //
- // @param {Object} obj Base object.
- // @param {Object} ext Object extending base.
- // @returns {String}
- function extendObjectValueArray( obj, ext ) {
- for ( const a in ext ) {
- if ( obj[ a ] ) {
- obj[ a ].push( ...ext[ a ] );
- } else {
- obj[ a ] = ext[ a ];
- }
- }
- }
- // A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
- // with content from {module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
- //
- // @param {module:ui/template~Template} def A template instance to be extended.
- // @param {module:ui/template~TemplateDefinition} def A definition which is to extend the template instance.
- function extendTemplate( template, def ) {
- if ( def.attributes ) {
- if ( !template.attributes ) {
- template.attributes = {};
- }
- extendObjectValueArray( template.attributes, def.attributes );
- }
- if ( def.eventListeners ) {
- if ( !template.eventListeners ) {
- template.eventListeners = {};
- }
- extendObjectValueArray( template.eventListeners, def.eventListeners );
- }
- if ( def.text ) {
- template.text.push( ...def.text );
- }
- if ( def.children && def.children.length ) {
- if ( template.children.length != def.children.length ) {
- /**
- * The number of children in extended definition does not match.
- *
- * @error ui-template-extend-children-mismatch
- */
- throw new CKEditorError(
- 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.'
- );
- }
- let childIndex = 0;
- for ( const childDef of def.children ) {
- extendTemplate( template.children[ childIndex++ ], childDef );
- }
- }
- }
- // Checks if value is "falsy".
- // Note: 0 (Number) is not "falsy" in this context.
- //
- // @private
- // @param {*} value Value to be checked.
- function isFalsy( value ) {
- return !value && value !== 0;
- }
- // Checks if the item is an instance of {@link module:ui/view~View}
- //
- // @private
- // @param {*} value Value to be checked.
- function isView( item ) {
- return item instanceof View;
- }
- // Checks if the item is an instance of {@link module:ui/template~Template}
- //
- // @private
- // @param {*} value Value to be checked.
- function isTemplate( item ) {
- return item instanceof Template;
- }
- // Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
- //
- // @private
- // @param {*} value Value to be checked.
- function isViewCollection( item ) {
- return item instanceof ViewCollection;
- }
- // Creates an empty skeleton for {@link module:ui/template~Template#revert}
- // data.
- //
- // @private
- function getEmptyRevertData() {
- return {
- children: [],
- bindings: [],
- attributes: {}
- };
- }
- // Checks whether an attribute should be extended when
- // {@link module:ui/template~Template#apply} is called.
- //
- // @private
- // @param {String} attrName Attribute name to check.
- function shouldExtend( attrName ) {
- return attrName == 'class' || attrName == 'style';
- }
- /**
- * A definition of the {@link module:ui/template~Template}. It describes what kind of
- * node a template will render (HTML element or text), attributes of an element, DOM event
- * listeners and children.
- *
- * Also see:
- * * {@link module:ui/template~TemplateValueSchema} to learn about HTML element attributes,
- * * {@link module:ui/template~TemplateListenerSchema} to learn about DOM event listeners.
- *
- * A sample definition on an HTML element can look like this:
- *
- * new Template( {
- * tag: 'p',
- * children: [
- * {
- * tag: 'span',
- * attributes: { ... },
- * children: [ ... ],
- * },
- * {
- * text: 'static–text'
- * },
- * 'also-static–text',
- * ],
- * attributes: {
- * class: {@link module:ui/template~TemplateValueSchema},
- * id: {@link module:ui/template~TemplateValueSchema},
- * style: {@link module:ui/template~TemplateValueSchema}
- *
- * // ...
- * },
- * on: {
- * 'click': {@link module:ui/template~TemplateListenerSchema}
- *
- * // Document.querySelector format is also accepted.
- * 'keyup@a.some-class': {@link module:ui/template~TemplateListenerSchema}
- *
- * // ...
- * }
- * } );
- *
- * A {@link module:ui/view~View}, another {@link module:ui/template~Template} or a native DOM node
- * can also become a child of a template. When a view is passed, its {@link module:ui/view~View#element} is used:
- *
- * const view = new SomeView();
- * const childTemplate = new Template( { ... } );
- * const childNode = document.createElement( 'b' );
- *
- * new Template( {
- * tag: 'p',
- *
- * children: [
- * // view#element will be added as a child of this <p>.
- * view,
- *
- * // The output of childTemplate.render() will be added here.
- * childTemplate,
- *
- * // Native DOM nodes are included directly in the rendered output.
- * childNode
- * ]
- * } );
- *
- * An entire {@link module:ui/viewcollection~ViewCollection} can be used as a child in the definition:
- *
- * const collection = new ViewCollection();
- * collection.add( someView );
- *
- * new Template( {
- * tag: 'p',
- *
- * children: collection
- * } );
- *
- * @typedef module:ui/template~TemplateDefinition
- * @type Object
- *
- * @property {String} tag See the template {@link module:ui/template~Template#tag} property.
- *
- * @property {Array.<module:ui/template~TemplateDefinition>} [children]
- * See the template {@link module:ui/template~Template#children} property.
- *
- * @property {Object.<String, module:ui/template~TemplateValueSchema>} [attributes]
- * See the template {@link module:ui/template~Template#attributes} property.
- *
- * @property {String|module:ui/template~TemplateValueSchema|Array.<String|module:ui/template~TemplateValueSchema>} [text]
- * See the template {@link module:ui/template~Template#text} property.
- *
- * @property {Object.<String, module:ui/template~TemplateListenerSchema>} [on]
- * See the template {@link module:ui/template~Template#eventListeners} property.
- */
- /**
- * Describes a value of an HTML element attribute or `textContent`. It allows combining multiple
- * data sources like static values and {@link module:utils/observablemixin~Observable} attributes.
- *
- * Also see:
- * * {@link module:ui/template~TemplateDefinition} to learn where to use it,
- * * {@link module:ui/template~Template.bind} to learn how to configure
- * {@link module:utils/observablemixin~Observable} attribute bindings,
- * * {@link module:ui/template~Template#render} to learn how to render a template,
- * * {@link module:ui/template~BindChain#to `to()`} and {@link module:ui/template~BindChain#if `if()`}
- * methods to learn more about bindings.
- *
- * Attribute values can be described in many different ways:
- *
- * // Bind helper will create bindings to attributes of the observable.
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * // A plain string schema.
- * 'class': 'static-text',
- *
- * // An object schema, binds to the "foo" attribute of the
- * // observable and follows its value.
- * 'class': bind.to( 'foo' ),
- *
- * // An array schema, combines the above.
- * 'class': [
- * 'static-text',
- * bind.to( 'bar', () => { ... } ),
- *
- * // Bindings can also be conditional.
- * bind.if( 'baz', 'class-when-baz-is-true' )
- * ],
- *
- * // An array schema, with a custom namespace, e.g. useful for creating SVGs.
- * 'class': {
- * ns: 'http://ns.url',
- * value: [
- * bind.if( 'baz', 'value-when-true' ),
- * 'static-text'
- * ]
- * },
- *
- * // An object schema, specific for styles.
- * style: {
- * color: 'red',
- * backgroundColor: bind.to( 'qux', () => { ... } )
- * }
- * }
- * } );
- *
- * Text nodes can also have complex values:
- *
- * const bind = Template.bind( observable, emitter );
- *
- * // Will render a "foo" text node.
- * new Template( {
- * text: 'foo'
- * } );
- *
- * // Will render a "static text: {observable.foo}" text node.
- * // The text of the node will be updated as the "foo" attribute changes.
- * new Template( {
- * text: [
- * 'static text: ',
- * bind.to( 'foo', () => { ... } )
- * ]
- * } );
- *
- * @typedef module:ui/template~TemplateValueSchema
- * @type {Object|String|Array}
- */
- /**
- * Describes an event listener attached to an HTML element. Such listener can propagate DOM events
- * through an {@link module:utils/observablemixin~Observable} instance, execute custom callbacks
- * or both, if necessary.
- *
- * Also see:
- * * {@link module:ui/template~TemplateDefinition} to learn more about template definitions,
- * * {@link module:ui/template~BindChain#to `to()`} method to learn more about bindings.
- *
- * Check out different ways of attaching event listeners below:
- *
- * // Bind helper will propagate events through the observable.
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * tag: 'p',
- * on: {
- * // An object schema. The observable will fire the "clicked" event upon DOM "click".
- * click: bind.to( 'clicked' )
- *
- * // An object schema. It will work for "click" event on "a.foo" children only.
- * 'click@a.foo': bind.to( 'clicked' )
- *
- * // An array schema, makes the observable propagate multiple events.
- * click: [
- * bind.to( 'clicked' ),
- * bind.to( 'executed' )
- * ],
- *
- * // An array schema with a custom callback.
- * 'click@a.foo': {
- * bind.to( 'clicked' ),
- * bind.to( evt => {
- * console.log( `${ evt.target } has been clicked!` );
- * } }
- * }
- * }
- * } );
- *
- * @typedef module:ui/template~TemplateListenerSchema
- * @type {Object|String|Array}
- */
- /**
- * The return value of {@link ~Template.bind `Template.bind()`}. It provides `to()` and `if()`
- * methods to create the {@link module:utils/observablemixin~Observable observable} attribute and event bindings.
- *
- * @interface module:ui/template~BindChain
- */
- /**
- * Binds an {@link module:utils/observablemixin~Observable observable} to either:
- *
- * * an HTML element attribute or a text node `textContent`, so it remains in sync with the observable
- * attribute as it changes,
- * * or an HTML element DOM event, so the DOM events are propagated through an observable.
- *
- * Some common use cases of `to()` bindings are presented below:
- *
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * // class="..." attribute gets bound to `observable#a`
- * class: bind.to( 'a' )
- * },
- * children: [
- * // <p>...</p> gets bound to observable#b; always `toUpperCase()`.
- * {
- * text: bind.to( 'b', ( value, node ) => value.toUpperCase() )
- * }
- * ],
- * on: {
- * click: [
- * // An observable will fire "clicked" upon "click" in the DOM.
- * bind.to( 'clicked' ),
- *
- * // A custom callback will be executed upon "click" in the DOM.
- * bind.to( () => {
- * ...
- * } )
- * ]
- * }
- * } ).render();
- *
- * Learn more about using `to()` in the {@link module:ui/template~TemplateValueSchema} and
- * {@link module:ui/template~TemplateListenerSchema}.
- *
- * @method #to
- * @param {String|Function} eventNameOrFunctionOrAttribute An attribute name of
- * {@link module:utils/observablemixin~Observable} or a DOM event name or an event callback.
- * @param {Function} [callback] Allows for processing of the value. Accepts `Node` and `value` as arguments.
- * @return {module:ui/template~TemplateBinding}
- */
- /**
- * Binds an {@link module:utils/observablemixin~Observable observable} to an HTML element attribute or a text
- * node `textContent` so it remains in sync with the observable attribute as it changes.
- *
- * Unlike {@link module:ui/template~BindChain#to}, it controls the presence of the attribute or `textContent`
- * depending on the "falseness" of an {@link module:utils/observablemixin~Observable} attribute.
- *
- * const bind = Template.bind( observable, emitter );
- *
- * new Template( {
- * tag: 'input',
- * attributes: {
- * // <input checked> when `observable#a` is not undefined/null/false/''
- * // <input> when `observable#a` is undefined/null/false
- * checked: bind.if( 'a' )
- * },
- * children: [
- * {
- * // <input>"b-is-not-set"</input> when `observable#b` is undefined/null/false/''
- * // <input></input> when `observable#b` is not "falsy"
- * text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
- * }
- * ]
- * } ).render();
- *
- * Learn more about using `if()` in the {@link module:ui/template~TemplateValueSchema}.
- *
- * @method #if
- * @param {String} attribute An attribute name of {@link module:utils/observablemixin~Observable} used in the binding.
- * @param {String} [valueIfTrue] Value set when the {@link module:utils/observablemixin~Observable} attribute is not
- * undefined/null/false/'' (empty string).
- * @param {Function} [callback] Allows for processing of the value. Accepts `Node` and `value` as arguments.
- * @return {module:ui/template~TemplateBinding}
- */
- /**
- * The {@link module:ui/template~Template#_renderNode} configuration.
- *
- * @private
- * @interface module:ui/template~RenderData
- */
- /**
- * Tells {@link module:ui/template~Template#_renderNode} to render
- * children into `DocumentFragment` first and then append the fragment
- * to the parent element. It is a speed optimization.
- *
- * @member {Boolean} #intoFragment
- */
- /**
- * A node which is being rendered.
- *
- * @member {HTMLElement|Text} #node
- */
- /**
- * Indicates whether the {@module:ui/template~RenderNodeOptions#node} has
- * been provided by {@module:ui/template~Template#apply}.
- *
- * @member {Boolean} #isApplying
- */
- /**
- * An object storing the data that helps {@module:ui/template~Template#revert}
- * bringing back an element to its initial state, i.e. before
- * {@module:ui/template~Template#apply} was called.
- *
- * @member {Object} #revertData
- */
|