8
0

template.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module ui/template
  7. */
  8. /* global document */
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  11. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  12. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  13. import View from './view';
  14. import ViewCollection from './viewcollection';
  15. import cloneDeepWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeepWith';
  16. import isObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isObject';
  17. import log from '@ckeditor/ckeditor5-utils/src/log';
  18. const xhtmlNs = 'http://www.w3.org/1999/xhtml';
  19. /**
  20. * A basic Template class. It renders DOM HTMLElement or Text from {@link module:ui/template~TemplateDefinition} and supports
  21. * element attributes, children, bindings to {@link module:utils/observablemixin~ObservableMixin} instances and DOM events
  22. * propagation. For example:
  23. *
  24. * new Template( {
  25. * tag: 'p',
  26. * attributes: {
  27. * class: 'foo',
  28. * style: {
  29. * backgroundColor: 'yellow'
  30. * }
  31. * },
  32. * children: [
  33. * 'A paragraph.'
  34. * ]
  35. * } ).render();
  36. *
  37. * will render the following HTMLElement:
  38. *
  39. * <p class="foo" style="background-color: yellow;">A paragraph.</p>
  40. *
  41. * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex template definitions.
  42. *
  43. * @mixes module:utils/emittermixin~EmitterMixin
  44. */
  45. export default class Template {
  46. /**
  47. * Creates an instance of the {@link ~Template} class.
  48. *
  49. * @param {module:ui/template~TemplateDefinition} def The definition of the template.
  50. */
  51. constructor( def ) {
  52. Object.assign( this, normalize( clone( def ) ) );
  53. /**
  54. * Indicates whether this particular Template instance has been
  55. * {@link #render rendered}.
  56. *
  57. * @readonly
  58. * @protected
  59. * @member {Boolean}
  60. */
  61. this._isRendered = false;
  62. /**
  63. * Tag of this template, i.e. `div`, indicating that the instance will render
  64. * to an HTMLElement.
  65. *
  66. * @member {String} #tag
  67. */
  68. /**
  69. * Text of this template, indicating that the instance will render to a DOM Text.
  70. *
  71. * @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
  72. */
  73. /**
  74. * Attributes of this template, i.e. `{ id: [ 'ck-id' ] }`, corresponding with
  75. * HTML attributes on HTMLElement.
  76. *
  77. * Note: Only when {@link #tag} is defined.
  78. *
  79. * @member {Object} #attributes
  80. */
  81. /**
  82. * Children of this template; sub–templates. Each one is an independent
  83. * instance of {@link ~Template}.
  84. *
  85. * Note: Only when {@link #tag} is defined.
  86. *
  87. * @member {module:utils/collection~Collection.<module:ui/template~Template>} #children
  88. */
  89. /**
  90. * DOM event listeners of this template.
  91. *
  92. * @member {Object} #eventListeners
  93. */
  94. /**
  95. * Data used by {@link #revert} method to restore a node
  96. * to its original state.
  97. *
  98. * See: {@link #apply}.
  99. *
  100. * @readonly
  101. * @protected
  102. * @member {module:ui/template~RenderData}
  103. */
  104. this._revertData = null;
  105. }
  106. /**
  107. * Renders a DOM Node (`HTMLElement` or `Text`) out of the template.
  108. *
  109. * @see #apply
  110. *
  111. * @returns {HTMLElement|Text}
  112. */
  113. render() {
  114. const node = this._renderNode( {
  115. intoFragment: true
  116. } );
  117. this._isRendered = true;
  118. return node;
  119. }
  120. /**
  121. * Applies the template to an existing DOM Node, either `HTMLElement` or `Text`.
  122. *
  123. * **Note:** No new DOM nodes (HTMLElement or Text) will be created. Applying extends attributes
  124. * ({@link module:ui/template~TemplateDefinition attributes}) and listeners ({@link module:ui/template~TemplateDefinition on}) only.
  125. *
  126. * **Note:** Existing "class" and "style" attributes are extended when a template
  127. * is applied to a Node, while other attributes and `textContent` are overridden.
  128. *
  129. * **Note:** The process of applying a template can be easily reverted using
  130. * {@link module:ui/template~Template#revert} method.
  131. *
  132. * const element = document.createElement( 'div' );
  133. * const bind = Template.bind( observableInstance, emitterInstance );
  134. *
  135. * new Template( {
  136. * attrs: {
  137. * id: 'first-div',
  138. * class: bind.to( 'divClass' )
  139. * },
  140. * on: {
  141. * click: bind( 'elementClicked' ) // Will be fired by the observableInstance.
  142. * }
  143. * children: [
  144. * 'Div text.'
  145. * ]
  146. * } ).apply( element );
  147. *
  148. * element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
  149. *
  150. * @see module:ui/template~Template#render
  151. * @see module:ui/template~Template#revert
  152. * @param {Node} node Root node for the template to apply.
  153. */
  154. apply( node ) {
  155. this._revertData = getEmptyRevertData();
  156. this._renderNode( {
  157. node: node,
  158. isApplying: true,
  159. revertData: this._revertData
  160. } );
  161. return node;
  162. }
  163. /**
  164. * Reverts a template {@link module:ui/template~Template#apply applied} to a DOM Node.
  165. *
  166. * @param {Node} node Root node for the template to revert. In most cases, it's the same node
  167. * that {@link module:ui/template~Template#apply} has used.
  168. */
  169. revert( node ) {
  170. if ( !this._revertData ) {
  171. /**
  172. * Attempting reverting a template which has not been applied yet.
  173. *
  174. * @error ui-template-revert-not-applied
  175. */
  176. throw new CKEditorError( 'ui-template-revert-not-applied: Attempting reverting a template which has not been applied yet.' );
  177. }
  178. this._revertTemplateFromNode( node, this._revertData );
  179. }
  180. /**
  181. * An entry point to the interface which allows binding DOM nodes to {@link module:utils/observablemixin~ObservableMixin}.
  182. * There are two types of bindings:
  183. *
  184. * * `HTMLElement` attributes or Text Node `textContent` can be synchronized with {@link module:utils/observablemixin~ObservableMixin}
  185. * instance attributes. See {@link module:ui/template~BindChain#to} and {@link module:ui/template~BindChain#if}.
  186. *
  187. * * DOM events fired on `HTMLElement` can be propagated through {@link module:utils/observablemixin~ObservableMixin}.
  188. * See {@link module:ui/template~BindChain#to}.
  189. *
  190. * @param {module:utils/observablemixin~ObservableMixin} observable An instance of ObservableMixin class.
  191. * @param {module:utils/emittermixin~EmitterMixin} emitter An instance of `Emitter` class. It listens
  192. * to `observable` attribute changes and DOM Events, depending on the binding. Usually {@link module:ui/view~View} instance.
  193. * @returns {module:ui/template~BindChain}
  194. */
  195. static bind( observable, emitter ) {
  196. return {
  197. to( eventNameOrFunctionOrAttribute, callback ) {
  198. return new TemplateToBinding( {
  199. eventNameOrFunction: eventNameOrFunctionOrAttribute,
  200. attribute: eventNameOrFunctionOrAttribute,
  201. observable, emitter, callback
  202. } );
  203. },
  204. if( attribute, valueIfTrue, callback ) {
  205. return new TemplateIfBinding( {
  206. observable, emitter, attribute, valueIfTrue, callback
  207. } );
  208. }
  209. };
  210. }
  211. /**
  212. * Extends {@link module:ui/template~Template} instance with additional content from {@link module:ui/template~TemplateDefinition}.
  213. *
  214. * const bind = Template.bind( observable, emitterInstance );
  215. * const instance = new Template( {
  216. * tag: 'p',
  217. * attributes: {
  218. * class: 'a',
  219. * data-x: bind.to( 'foo' )
  220. * },
  221. * children: [
  222. * {
  223. * tag: 'span',
  224. * attributes: {
  225. * class: 'b'
  226. * },
  227. * children: [
  228. * 'Span'
  229. * ]
  230. * }
  231. * ]
  232. * } );
  233. *
  234. * // Instance-level extension.
  235. * Template.extend( instance, {
  236. * attributes: {
  237. * class: 'b',
  238. * data-x: bind.to( 'bar' )
  239. * },
  240. * children: [
  241. * {
  242. * attributes: {
  243. * class: 'c'
  244. * }
  245. * }
  246. * ]
  247. * } );
  248. *
  249. * // Child extension.
  250. * Template.extend( instance.children.get( 0 ), {
  251. * attributes: {
  252. * class: 'd'
  253. * }
  254. * } );
  255. *
  256. * the `instance.render().outerHTML` is
  257. *
  258. * <p class="a b" data-x="{ observable.foo } { observable.bar }">
  259. * <span class="b c d">Span</span>
  260. * </p>
  261. *
  262. * @param {module:ui/template~Template} template Existing Template instance to be extended.
  263. * @param {module:ui/template~TemplateDefinition} def An extension to existing an template instance.
  264. */
  265. static extend( template, def ) {
  266. if ( template._isRendered ) {
  267. /**
  268. * Extending a template after rendering may not work as expected. To make sure
  269. * the {@link #extend extending} works for the rendered element, perform it
  270. * before {@link #render} is called.
  271. *
  272. * @error template-extend-render
  273. */
  274. log.warn( 'template-extend-render: Attempting to extend a template which has already been rendered.' );
  275. }
  276. extendTemplate( template, normalize( clone( def ) ) );
  277. }
  278. /**
  279. * Renders a DOM Node (either `HTMLElement` or `Text`) out of the template.
  280. *
  281. * @protected
  282. * @param {module:ui/template~RenderData} data Rendering data.
  283. */
  284. _renderNode( data ) {
  285. let isInvalid;
  286. if ( data.node ) {
  287. // When applying, a definition cannot have "tag" and "text" at the same time.
  288. isInvalid = this.tag && this.text;
  289. } else {
  290. // When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
  291. isInvalid = this.tag ? this.text : !this.text;
  292. }
  293. if ( isInvalid ) {
  294. /**
  295. * Node definition cannot have "tag" and "text" properties at the same time.
  296. * Node definition must have either "tag" or "text" when rendering new Node.
  297. *
  298. * @error ui-template-wrong-syntax
  299. */
  300. throw new CKEditorError( 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering new Node.' );
  301. }
  302. if ( this.text ) {
  303. return this._renderText( data );
  304. } else {
  305. return this._renderElement( data );
  306. }
  307. }
  308. /**
  309. * Renders an `HTMLElement` out of the template.
  310. *
  311. * @protected
  312. * @param {module:ui/template~RenderData} data Rendering data.
  313. */
  314. _renderElement( data ) {
  315. let node = data.node;
  316. if ( !node ) {
  317. node = data.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
  318. }
  319. this._renderAttributes( data );
  320. this._renderElementChildren( data );
  321. this._setUpListeners( data );
  322. return node;
  323. }
  324. /**
  325. * Renders a `Text` node out of {@link module:ui/template~Template#text}.
  326. *
  327. * @protected
  328. * @param {module:ui/template~RenderData} data Rendering data.
  329. */
  330. _renderText( data ) {
  331. let node = data.node;
  332. // Save the original textContent to revert it in #revert().
  333. if ( node ) {
  334. data.revertData.text = node.textContent;
  335. } else {
  336. node = data.node = document.createTextNode( '' );
  337. }
  338. // Check if this Text Node is bound to Observable. Cases:
  339. //
  340. // text: [ Template.bind( ... ).to( ... ) ]
  341. //
  342. // text: [
  343. // 'foo',
  344. // Template.bind( ... ).to( ... ),
  345. // ...
  346. // ]
  347. //
  348. if ( hasTemplateBinding( this.text ) ) {
  349. this._bindToObservable( {
  350. schema: this.text,
  351. updater: getTextUpdater( node ),
  352. data
  353. } );
  354. }
  355. // Simply set text. Cases:
  356. //
  357. // text: [ 'all', 'are', 'static' ]
  358. //
  359. // text: [ 'foo' ]
  360. //
  361. else {
  362. node.textContent = this.text.join( '' );
  363. }
  364. return node;
  365. }
  366. /**
  367. * Renders an `HTMLElement` attributes out of {@link module:ui/template~Template#attributes}.
  368. *
  369. * @protected
  370. * @param {module:ui/template~RenderData} data Rendering data.
  371. */
  372. _renderAttributes( data ) {
  373. let attrName, attrValue, domAttrValue, attrNs;
  374. if ( !this.attributes ) {
  375. return;
  376. }
  377. const node = data.node;
  378. const revertData = data.revertData;
  379. for ( attrName in this.attributes ) {
  380. // Current attribute value in DOM.
  381. domAttrValue = node.getAttribute( attrName );
  382. // The value to be set.
  383. attrValue = this.attributes[ attrName ];
  384. // Save revert data.
  385. if ( revertData ) {
  386. revertData.attributes[ attrName ] = domAttrValue;
  387. }
  388. // Detect custom namespace:
  389. //
  390. // class: {
  391. // ns: 'abc',
  392. // value: Template.bind( ... ).to( ... )
  393. // }
  394. //
  395. attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;
  396. // Activate binding if one is found. Cases:
  397. //
  398. // class: [
  399. // Template.bind( ... ).to( ... )
  400. // ]
  401. //
  402. // class: [
  403. // 'bar',
  404. // Template.bind( ... ).to( ... ),
  405. // 'baz'
  406. // ]
  407. //
  408. // class: {
  409. // ns: 'abc',
  410. // value: Template.bind( ... ).to( ... )
  411. // }
  412. //
  413. if ( hasTemplateBinding( attrValue ) ) {
  414. // Normalize attributes with additional data like namespace:
  415. //
  416. // class: {
  417. // ns: 'abc',
  418. // value: [ ... ]
  419. // }
  420. //
  421. const valueToBind = attrNs ? attrValue[ 0 ].value : attrValue;
  422. // Extend the original value of attributes like "style" and "class",
  423. // don't override them.
  424. if ( revertData && shouldExtend( attrName ) ) {
  425. valueToBind.unshift( domAttrValue );
  426. }
  427. this._bindToObservable( {
  428. schema: valueToBind,
  429. updater: getAttributeUpdater( node, attrName, attrNs ),
  430. data
  431. } );
  432. }
  433. // Style attribute could be an Object so it needs to be parsed in a specific way.
  434. //
  435. // style: {
  436. // width: '100px',
  437. // height: Template.bind( ... ).to( ... )
  438. // }
  439. //
  440. else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
  441. this._renderStyleAttribute( attrValue[ 0 ], data );
  442. }
  443. // Otherwise simply set the static attribute:
  444. //
  445. // class: [ 'foo' ]
  446. //
  447. // class: [ 'all', 'are', 'static' ]
  448. //
  449. // class: [
  450. // {
  451. // ns: 'abc',
  452. // value: [ 'foo' ]
  453. // }
  454. // ]
  455. //
  456. else {
  457. // Extend the original value of attributes like "style" and "class",
  458. // don't override them.
  459. if ( revertData && domAttrValue && shouldExtend( attrName ) ) {
  460. attrValue.unshift( domAttrValue );
  461. }
  462. attrValue = attrValue
  463. // Retrieve "values" from:
  464. //
  465. // class: [
  466. // {
  467. // ns: 'abc',
  468. // value: [ ... ]
  469. // }
  470. // ]
  471. //
  472. .map( val => val ? ( val.value || val ) : val )
  473. // Flatten the array.
  474. .reduce( ( prev, next ) => prev.concat( next ), [] )
  475. // Convert into string.
  476. .reduce( arrayValueReducer, '' );
  477. if ( !isFalsy( attrValue ) ) {
  478. node.setAttributeNS( attrNs, attrName, attrValue );
  479. }
  480. }
  481. }
  482. }
  483. /**
  484. * Renders `style` attribute of an `HTMLElement` based on {@link module:ui/template~Template#attributes}.
  485. *
  486. * Style attribute is an {Object} with static values:
  487. *
  488. * attributes: {
  489. * style: {
  490. * color: 'red'
  491. * }
  492. * }
  493. *
  494. * or values bound to {@link module:ui/model~Model} properties:
  495. *
  496. * attributes: {
  497. * style: {
  498. * color: bind.to( ... )
  499. * }
  500. * }
  501. *
  502. * Note: `style` attribute is rendered without setting the namespace. It does not seem to be
  503. * needed.
  504. *
  505. * @private
  506. * @param {Object} styles Styles located in `attributes.style` of {@link module:ui/template~TemplateDefinition}.
  507. * @param {module:ui/template~RenderData} data Rendering data.
  508. */
  509. _renderStyleAttribute( styles, data ) {
  510. const node = data.node;
  511. for ( let styleName in styles ) {
  512. const styleValue = styles[ styleName ];
  513. // Cases:
  514. //
  515. // style: {
  516. // color: bind.to( 'attribute' )
  517. // }
  518. //
  519. if ( hasTemplateBinding( styleValue ) ) {
  520. this._bindToObservable( {
  521. schema: [ styleValue ],
  522. updater: getStyleUpdater( node, styleName ),
  523. data
  524. } );
  525. }
  526. // Cases:
  527. //
  528. // style: {
  529. // color: 'red'
  530. // }
  531. //
  532. else {
  533. node.style[ styleName ] = styleValue;
  534. }
  535. }
  536. }
  537. /**
  538. * Recursively renders `HTMLElement` children from {@link module:ui/template~Template#children}.
  539. *
  540. * @protected
  541. * @param {module:ui/template~RenderData} data Rendering data.
  542. */
  543. _renderElementChildren( data ) {
  544. const node = data.node;
  545. const container = data.intoFragment ? document.createDocumentFragment() : node;
  546. const isApplying = data.isApplying;
  547. let childIndex = 0;
  548. for ( let child of this.children ) {
  549. if ( isViewCollection( child ) ) {
  550. if ( !isApplying ) {
  551. child.setParent( node );
  552. for ( let view of child ) {
  553. container.appendChild( view.element );
  554. }
  555. }
  556. } else if ( isView( child ) ) {
  557. if ( !isApplying ) {
  558. container.appendChild( child.element );
  559. }
  560. } else {
  561. if ( isApplying ) {
  562. const revertData = data.revertData;
  563. const childRevertData = getEmptyRevertData();
  564. revertData.children.push( childRevertData );
  565. child._renderNode( {
  566. node: container.childNodes[ childIndex++ ],
  567. isApplying: true,
  568. revertData: childRevertData
  569. } );
  570. } else {
  571. container.appendChild( child.render() );
  572. }
  573. }
  574. }
  575. if ( data.intoFragment ) {
  576. node.appendChild( container );
  577. }
  578. }
  579. /**
  580. * Activates `on` listeners in the {@link module:ui/template~TemplateDefinition}
  581. * on a passed `HTMLElement`.
  582. *
  583. * @protected
  584. * @param {module:ui/template~RenderData} data Rendering data.
  585. */
  586. _setUpListeners( data ) {
  587. if ( !this.eventListeners ) {
  588. return;
  589. }
  590. for ( let key in this.eventListeners ) {
  591. const revertBindings = this.eventListeners[ key ].map( schemaItem => {
  592. const [ domEvtName, domSelector ] = key.split( '@' );
  593. return schemaItem.activateDomEventListener( domEvtName, domSelector, data );
  594. } );
  595. if ( data.revertData ) {
  596. data.revertData.bindings.push( revertBindings );
  597. }
  598. }
  599. }
  600. /**
  601. * For given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding} it activates the
  602. * binding and sets its initial value.
  603. *
  604. * Note: {@link module:ui/template~TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
  605. *
  606. * @protected
  607. * @param {Object} options Binding options.
  608. * @param {module:ui/template~TemplateValueSchema} options.schema
  609. * @param {Function} options.updater A function which updates DOM (like attribute or text).
  610. * @param {module:ui/template~RenderData} options.data Rendering data.
  611. */
  612. _bindToObservable( { schema, updater, data } ) {
  613. const revertData = data.revertData;
  614. // Set initial values.
  615. syncValueSchemaValue( schema, updater, data );
  616. const revertBindings = schema
  617. // Filter "falsy" (false, undefined, null, '') value schema components out.
  618. .filter( item => !isFalsy( item ) )
  619. // Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
  620. .filter( item => item.observable )
  621. // Once only the actual binding are left, let the emitter listen to observable change:attribute event.
  622. // TODO: Reduce the number of listeners attached as many bindings may listen
  623. // to the same observable attribute.
  624. .map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );
  625. if ( revertData ) {
  626. revertData.bindings.push( revertBindings );
  627. }
  628. }
  629. /**
  630. * Reverts {@link module:ui/template~RenderData#revertData template data} from a node to
  631. * return it to the the original state.
  632. *
  633. * @protected
  634. * @param {HTMLElement|Text} node A node to be reverted.
  635. * @param {module:ui/template~RenderData#revertData} revertData Stores information about
  636. * what changes have been made by {@link #apply} to the node.
  637. */
  638. _revertTemplateFromNode( node, revertData ) {
  639. for ( let binding of revertData.bindings ) {
  640. // Each binding may consist of several observable+observable#attribute.
  641. // like the following has 2:
  642. //
  643. // class: [
  644. // 'x',
  645. // bind.to( 'foo' ),
  646. // 'y',
  647. // bind.to( 'bar' )
  648. // ]
  649. //
  650. for ( let revertBinding of binding ) {
  651. revertBinding();
  652. }
  653. }
  654. if ( revertData.text ) {
  655. node.textContent = revertData.text;
  656. return;
  657. }
  658. for ( let attrName in revertData.attributes ) {
  659. const attrValue = revertData.attributes[ attrName ];
  660. // When the attribute has **not** been set before #apply().
  661. if ( attrValue === null ) {
  662. node.removeAttribute( attrName );
  663. } else {
  664. node.setAttribute( attrName, attrValue );
  665. }
  666. }
  667. for ( let i = 0; i < revertData.children.length; ++i ) {
  668. this._revertTemplateFromNode( node.childNodes[ i ], revertData.children[ i ] );
  669. }
  670. }
  671. }
  672. mix( Template, EmitterMixin );
  673. /**
  674. * Describes a binding created by {@link module:ui/template~Template.bind} interface.
  675. *
  676. * @protected
  677. */
  678. export class TemplateBinding {
  679. /**
  680. * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
  681. *
  682. * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
  683. */
  684. constructor( def ) {
  685. Object.assign( this, def );
  686. /**
  687. * An observable instance of the binding. It provides the attribute
  688. * with the value or passes the event when a corresponding DOM event is fired.
  689. *
  690. * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
  691. */
  692. /**
  693. * An {@link module:utils/emittermixin~EmitterMixin} instance used by the binding
  694. * to (either):
  695. *
  696. * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
  697. * * listen to the event in the DOM.
  698. *
  699. * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
  700. */
  701. /**
  702. * The name of the attribute of {@link module:ui/template~TemplateBinding#observable} which is observed.
  703. *
  704. * @member {String} module:ui/template~TemplateBinding#attribute
  705. */
  706. /**
  707. * A custom function to process the value of {@link module:ui/template~TemplateBinding#attribute}.
  708. *
  709. * @member {Function} [module:ui/template~TemplateBinding#callback]
  710. */
  711. }
  712. /**
  713. * Returns the value of the binding, which is the value of {@link module:ui/template~TemplateBinding#attribute} in
  714. * {@link module:ui/template~TemplateBinding#observable}.
  715. *
  716. * @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
  717. * @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in {@link module:ui/template~TemplateBinding#observable}.
  718. */
  719. getValue( node ) {
  720. const value = this.observable[ this.attribute ];
  721. return this.callback ? this.callback( value, node ) : value;
  722. }
  723. /**
  724. * Activates the listener for the changes of {@link module:ui/template~TemplateBinding#attribute} in
  725. * {@link module:ui/template~TemplateBinding#observable}, which then updates the DOM with the aggregated
  726. * value of {@link module:ui/template~TemplateValueSchema}.
  727. *
  728. * For instance, the `class` attribute of the `Template` element can be be bound to
  729. * the observable `foo` attribute in `ObservableMixin` instance.
  730. *
  731. * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in DOM.
  732. * @param {Function} updater A DOM updater function used to update native DOM attribute or text.
  733. * @param {module:ui/template~RenderData} data Rendering data.
  734. * @returns {Function} A function to sever the listener binding.
  735. */
  736. activateAttributeListener( schema, updater, data ) {
  737. const callback = () => syncValueSchemaValue( schema, updater, data );
  738. this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );
  739. // Allows revert of the listener.
  740. return () => {
  741. this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
  742. };
  743. }
  744. }
  745. /**
  746. * Describes either:
  747. *
  748. * * a binding to {@link module:utils/observablemixin~ObservableMixin}
  749. * * or a native DOM event binding
  750. *
  751. * created by {@link module:ui/template~BindChain#to} method.
  752. *
  753. * @protected
  754. */
  755. export class TemplateToBinding extends TemplateBinding {
  756. /**
  757. * Activates the listener for the native DOM event, which when fired, is propagated by
  758. * the {@link module:ui/template~TemplateBinding#emitter}.
  759. *
  760. * @param {String} domEvtName A name of the native DOM event.
  761. * @param {String} domSelector A selector in DOM to filter delegated events.
  762. * @param {module:ui/template~RenderData} data Rendering data.
  763. * @returns {Function} A function to sever the listener binding.
  764. */
  765. activateDomEventListener( domEvtName, domSelector, data ) {
  766. const callback = ( evt, domEvt ) => {
  767. if ( !domSelector || domEvt.target.matches( domSelector ) ) {
  768. if ( typeof this.eventNameOrFunction == 'function' ) {
  769. this.eventNameOrFunction( domEvt );
  770. } else {
  771. this.observable.fire( this.eventNameOrFunction, domEvt );
  772. }
  773. }
  774. };
  775. this.emitter.listenTo( data.node, domEvtName, callback );
  776. // Allows revert of the listener.
  777. return () => {
  778. this.emitter.stopListening( data.node, domEvtName, callback );
  779. };
  780. }
  781. }
  782. /**
  783. * Describes a binding to {@link module:utils/observablemixin~ObservableMixin} created by {@link module:ui/template~BindChain#if}
  784. * method.
  785. *
  786. * @protected
  787. */
  788. export class TemplateIfBinding extends TemplateBinding {
  789. /**
  790. * @inheritDoc
  791. */
  792. getValue( node ) {
  793. const value = super.getValue( node );
  794. return isFalsy( value ) ? false : ( this.valueIfTrue || true );
  795. }
  796. /**
  797. * The value of the DOM attribute/text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
  798. * {@link module:ui/template~TemplateBinding#observable} is `true`.
  799. *
  800. * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
  801. */
  802. }
  803. // Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
  804. // {@link module:ui/template~TemplateBinding}.
  805. //
  806. // @param {module:ui/template~TemplateValueSchema} schema
  807. // @returns {Boolean}
  808. function hasTemplateBinding( schema ) {
  809. if ( !schema ) {
  810. return false;
  811. }
  812. // Normalize attributes with additional data like namespace:
  813. //
  814. // class: {
  815. // ns: 'abc',
  816. // value: [ ... ]
  817. // }
  818. //
  819. if ( schema.value ) {
  820. schema = schema.value;
  821. }
  822. if ( Array.isArray( schema ) ) {
  823. return schema.some( hasTemplateBinding );
  824. } else if ( schema instanceof TemplateBinding ) {
  825. return true;
  826. }
  827. return false;
  828. }
  829. // Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
  830. // an Array. Each entry of an Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
  831. // items.
  832. //
  833. // @param {module:ui/template~TemplateValueSchema} schema
  834. // @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
  835. // @return {Array}
  836. function getValueSchemaValue( schema, node ) {
  837. return schema.map( schemaItem => {
  838. // Process {@link module:ui/template~TemplateBinding} bindings.
  839. if ( schemaItem instanceof TemplateBinding ) {
  840. return schemaItem.getValue( node );
  841. }
  842. // All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
  843. return schemaItem;
  844. } );
  845. }
  846. // A function executed each time bound Observable attribute changes, which updates DOM with a value
  847. // constructed from {@link module:ui/template~TemplateValueSchema}.
  848. //
  849. // @param {module:ui/template~TemplateValueSchema} schema
  850. // @param {Function} updater A function which updates DOM (like attribute or text).
  851. // @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
  852. function syncValueSchemaValue( schema, updater, { node } ) {
  853. let value = getValueSchemaValue( schema, node );
  854. // Check if schema is a single Template.bind.if, like:
  855. //
  856. // class: Template.bind.if( 'foo' )
  857. //
  858. if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
  859. value = value[ 0 ];
  860. } else {
  861. value = value.reduce( arrayValueReducer, '' );
  862. }
  863. if ( isFalsy( value ) ) {
  864. updater.remove();
  865. } else {
  866. updater.set( value );
  867. }
  868. }
  869. // Returns an object consisting of `set` and `remove` functions, which
  870. // can be used in the context of DOM Node to set or reset `textContent`.
  871. // @see module:ui/view~View#_bindToObservable
  872. //
  873. // @param {Node} node DOM Node to be modified.
  874. // @returns {Object}
  875. function getTextUpdater( node ) {
  876. return {
  877. set( value ) {
  878. node.textContent = value;
  879. },
  880. remove() {
  881. node.textContent = '';
  882. }
  883. };
  884. }
  885. // Returns an object consisting of `set` and `remove` functions, which
  886. // can be used in the context of DOM Node to set or reset an attribute.
  887. // @see module:ui/view~View#_bindToObservable
  888. //
  889. // @param {Node} node DOM Node to be modified.
  890. // @param {String} attrName Name of the attribute to be modified.
  891. // @param {String} [ns=null] Namespace to use.
  892. // @returns {Object}
  893. function getAttributeUpdater( el, attrName, ns ) {
  894. return {
  895. set( value ) {
  896. el.setAttributeNS( ns, attrName, value );
  897. },
  898. remove() {
  899. el.removeAttributeNS( ns, attrName );
  900. }
  901. };
  902. }
  903. // Returns an object consisting of `set` and `remove` functions, which
  904. // can be used in the context of CSSStyleDeclaration to set or remove a style.
  905. // @see module:ui/view~View#_bindToObservable
  906. //
  907. // @param {Node} node DOM Node to be modified.
  908. // @param {String} styleName Name of the style to be modified.
  909. // @returns {Object}
  910. function getStyleUpdater( el, styleName ) {
  911. return {
  912. set( value ) {
  913. el.style[ styleName ] = value;
  914. },
  915. remove() {
  916. el.style[ styleName ] = null;
  917. }
  918. };
  919. }
  920. // Clones definition of the template.
  921. //
  922. // @param {module:ui/template~TemplateDefinition} def
  923. // @returns {module:ui/template~TemplateDefinition}
  924. function clone( def ) {
  925. const clone = cloneDeepWith( def, value => {
  926. // Don't clone the `Template.bind`* bindings because of the references to Observable
  927. // and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
  928. // cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
  929. // along with the definition.
  930. //
  931. // Don't clone Template instances if provided as a child. They're simply #render()ed
  932. // and nothing should interfere.
  933. //
  934. // Also don't clone View instances if provided as a child of the Template. The template
  935. // instance will be extracted from the View during the normalization and there's no need
  936. // to clone it.
  937. if ( value && ( value instanceof TemplateBinding || isTemplate( value ) || isView( value ) || isViewCollection( value ) ) ) {
  938. return value;
  939. }
  940. } );
  941. return clone;
  942. }
  943. // Normalizes given {@link module:ui/template~TemplateDefinition}.
  944. //
  945. // See:
  946. // * {@link normalizeAttributes}
  947. // * {@link normalizeListeners}
  948. // * {@link normalizePlainTextDefinition}
  949. // * {@link normalizeTextDefinition}
  950. //
  951. // @param {module:ui/template~TemplateDefinition} def
  952. // @returns {module:ui/template~TemplateDefinition} Normalized definition.
  953. function normalize( def ) {
  954. if ( typeof def == 'string' ) {
  955. def = normalizePlainTextDefinition( def );
  956. } else if ( def.text ) {
  957. normalizeTextDefinition( def );
  958. }
  959. if ( def.on ) {
  960. def.eventListeners = normalizeListeners( def.on );
  961. // Template mixes EmitterMixin, so delete #on to avoid collision.
  962. delete def.on;
  963. }
  964. if ( !def.text ) {
  965. if ( def.attributes ) {
  966. normalizeAttributes( def.attributes );
  967. }
  968. const children = new Collection();
  969. if ( def.children ) {
  970. if ( isViewCollection( def.children ) ) {
  971. children.add( def.children );
  972. } else {
  973. for ( let child of def.children ) {
  974. if ( isTemplate( child ) || isView( child ) ) {
  975. children.add( child );
  976. } else {
  977. children.add( new Template( child ) );
  978. }
  979. }
  980. }
  981. }
  982. def.children = children;
  983. }
  984. return def;
  985. }
  986. // Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
  987. //
  988. // attributes: {
  989. // a: 'bar',
  990. // b: {@link module:ui/template~TemplateBinding},
  991. // c: {
  992. // value: 'bar'
  993. // }
  994. // }
  995. //
  996. // becomes
  997. //
  998. // attributes: {
  999. // a: [ 'bar' ],
  1000. // b: [ {@link module:ui/template~TemplateBinding} ],
  1001. // c: {
  1002. // value: [ 'bar' ]
  1003. // }
  1004. // }
  1005. //
  1006. // @param {Object} attrs
  1007. function normalizeAttributes( attrs ) {
  1008. for ( let a in attrs ) {
  1009. if ( attrs[ a ].value ) {
  1010. attrs[ a ].value = [].concat( attrs[ a ].value );
  1011. }
  1012. arrayify( attrs, a );
  1013. }
  1014. }
  1015. // Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
  1016. //
  1017. // on: {
  1018. // a: 'bar',
  1019. // b: {@link module:ui/template~TemplateBinding},
  1020. // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
  1021. // }
  1022. //
  1023. // becomes
  1024. //
  1025. // on: {
  1026. // a: [ 'bar' ],
  1027. // b: [ {@link module:ui/template~TemplateBinding} ],
  1028. // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
  1029. // }
  1030. //
  1031. // @param {Object} listeners
  1032. // @returns {Object} Object containing normalized listeners.
  1033. function normalizeListeners( listeners ) {
  1034. for ( let l in listeners ) {
  1035. arrayify( listeners, l );
  1036. }
  1037. return listeners;
  1038. }
  1039. // Normalizes "string" {@link module:ui/template~TemplateDefinition}.
  1040. //
  1041. // "foo"
  1042. //
  1043. // becomes
  1044. //
  1045. // { text: [ 'foo' ] },
  1046. //
  1047. // @param {String} def
  1048. // @returns {module:ui/template~TemplateDefinition} Normalized template definition.
  1049. function normalizePlainTextDefinition( def ) {
  1050. return {
  1051. text: [ def ]
  1052. };
  1053. }
  1054. // Normalizes text {@link module:ui/template~TemplateDefinition}.
  1055. //
  1056. // children: [
  1057. // { text: 'def' },
  1058. // { text: {@link module:ui/template~TemplateBinding} }
  1059. // ]
  1060. //
  1061. // becomes
  1062. //
  1063. // children: [
  1064. // { text: [ 'def' ] },
  1065. // { text: [ {@link module:ui/template~TemplateBinding} ] }
  1066. // ]
  1067. //
  1068. // @param {module:ui/template~TemplateDefinition} def
  1069. function normalizeTextDefinition( def ) {
  1070. if ( !Array.isArray( def.text ) ) {
  1071. def.text = [ def.text ];
  1072. }
  1073. }
  1074. // Wraps an entry in Object in an Array, if not already one.
  1075. //
  1076. // {
  1077. // x: 'y',
  1078. // a: [ 'b' ]
  1079. // }
  1080. //
  1081. // becomes
  1082. //
  1083. // {
  1084. // x: [ 'y' ],
  1085. // a: [ 'b' ]
  1086. // }
  1087. //
  1088. // @param {Object} obj
  1089. // @param {String} key
  1090. function arrayify( obj, key ) {
  1091. if ( !Array.isArray( obj[ key ] ) ) {
  1092. obj[ key ] = [ obj[ key ] ];
  1093. }
  1094. }
  1095. // A helper which concatenates the value avoiding unwanted
  1096. // leading white spaces.
  1097. //
  1098. // @param {String} prev
  1099. // @param {String} cur
  1100. // @returns {String}
  1101. function arrayValueReducer( prev, cur ) {
  1102. if ( isFalsy( cur ) ) {
  1103. return prev;
  1104. } else if ( isFalsy( prev ) ) {
  1105. return cur;
  1106. } else {
  1107. return `${prev} ${cur}`;
  1108. }
  1109. }
  1110. // Extends one object defined in the following format:
  1111. //
  1112. // {
  1113. // key1: [Array1],
  1114. // key2: [Array2],
  1115. // ...
  1116. // keyN: [ArrayN]
  1117. // }
  1118. //
  1119. // with another object of the same data format.
  1120. //
  1121. // @param {Object} obj Base object.
  1122. // @param {Object} ext Object extending base.
  1123. // @returns {String}
  1124. function extendObjectValueArray( obj, ext ) {
  1125. for ( let a in ext ) {
  1126. if ( obj[ a ] ) {
  1127. obj[ a ].push( ...ext[ a ] );
  1128. } else {
  1129. obj[ a ] = ext[ a ];
  1130. }
  1131. }
  1132. }
  1133. // A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
  1134. // with content from {module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
  1135. //
  1136. // @param {module:ui/template~Template} def A template instance to be extended.
  1137. // @param {module:ui/template~TemplateDefinition} def A definition which is to extend the template instance.
  1138. function extendTemplate( template, def ) {
  1139. if ( def.attributes ) {
  1140. if ( !template.attributes ) {
  1141. template.attributes = {};
  1142. }
  1143. extendObjectValueArray( template.attributes, def.attributes );
  1144. }
  1145. if ( def.eventListeners ) {
  1146. if ( !template.eventListeners ) {
  1147. template.eventListeners = {};
  1148. }
  1149. extendObjectValueArray( template.eventListeners, def.eventListeners );
  1150. }
  1151. if ( def.text ) {
  1152. template.text.push( ...def.text );
  1153. }
  1154. if ( def.children && def.children.length ) {
  1155. if ( template.children.length != def.children.length ) {
  1156. /**
  1157. * The number of children in extended definition does not match.
  1158. *
  1159. * @error ui-template-extend-children-mismatch
  1160. */
  1161. throw new CKEditorError( 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.' );
  1162. }
  1163. let childIndex = 0;
  1164. for ( let childDef of def.children ) {
  1165. extendTemplate( template.children.get( childIndex++ ), childDef );
  1166. }
  1167. }
  1168. }
  1169. // Checks if value is "falsy".
  1170. // Note: 0 (Number) is not "falsy" in this context.
  1171. //
  1172. // @private
  1173. // @param {*} value Value to be checked.
  1174. function isFalsy( value ) {
  1175. return !value && value !== 0;
  1176. }
  1177. // Checks if the item is an instance of {@link module:ui/view~View}
  1178. //
  1179. // @private
  1180. // @param {*} value Value to be checked.
  1181. function isView( item ) {
  1182. return item instanceof View;
  1183. }
  1184. // Checks if the item is an instance of {@link module:ui/template~Template}
  1185. //
  1186. // @private
  1187. // @param {*} value Value to be checked.
  1188. function isTemplate( item ) {
  1189. return item instanceof Template;
  1190. }
  1191. // Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
  1192. //
  1193. // @private
  1194. // @param {*} value Value to be checked.
  1195. function isViewCollection( item ) {
  1196. return item instanceof ViewCollection;
  1197. }
  1198. // Creates an empty skeleton for {@link module:ui/template~Template#revert}
  1199. // data.
  1200. //
  1201. // @private
  1202. function getEmptyRevertData() {
  1203. return {
  1204. children: [],
  1205. bindings: [],
  1206. attributes: {}
  1207. };
  1208. }
  1209. // Checks whether an attribute should be extended when
  1210. // {@link module:ui/template~Template#apply} is called.
  1211. //
  1212. // @private
  1213. // @param {String} attrName Attribute name to check.
  1214. function shouldExtend( attrName ) {
  1215. return attrName == 'class' || attrName == 'style';
  1216. }
  1217. /**
  1218. * A definition of {@link module:ui/template~Template}.
  1219. * See: {@link module:ui/template~TemplateValueSchema}.
  1220. *
  1221. * new Template( {
  1222. * tag: 'p',
  1223. * children: [
  1224. * {
  1225. * tag: 'span',
  1226. * attributes: { ... },
  1227. * children: [ ... ],
  1228. * ...
  1229. * },
  1230. * {
  1231. * text: 'static–text'
  1232. * },
  1233. * 'also-static–text',
  1234. * <{@link module:ui/view~View} instance>
  1235. * <{@link module:ui/template~Template} instance>
  1236. * ...
  1237. * ],
  1238. * attributes: {
  1239. * class: {@link module:ui/template~TemplateValueSchema},
  1240. * id: {@link module:ui/template~TemplateValueSchema},
  1241. * style: {@link module:ui/template~TemplateValueSchema}
  1242. * ...
  1243. * },
  1244. * on: {
  1245. * 'click': {@link module:ui/template~TemplateListenerSchema}
  1246. * 'keyup@.some-class': {@link module:ui/template~TemplateListenerSchema},
  1247. * ...
  1248. * }
  1249. * } );
  1250. *
  1251. * // An entire view collection can be used as a child in the definition.
  1252. * new Template( {
  1253. * tag: 'p',
  1254. * children: <{@link module:ui/viewcollection~ViewCollection} instance>
  1255. * } );
  1256. *
  1257. * @typedef module:ui/template~TemplateDefinition
  1258. * @type Object
  1259. * @property {String} tag
  1260. * @property {Array.<module:ui/template~TemplateDefinition>} [children]
  1261. * @property {Object.<String,module:ui/template~TemplateValueSchema>} [attributes]
  1262. * @property {String|module:ui/template~TemplateValueSchema|Array.<String|module:ui/template~TemplateValueSchema>} [text]
  1263. * @property {Object.<String,module:ui/template~TemplateListenerSchema>} [on]
  1264. */
  1265. /**
  1266. * Describes a value of HTMLElement attribute or `textContent`. See:
  1267. * * {@link module:ui/template~TemplateDefinition},
  1268. * * {@link module:ui/template~Template.bind},
  1269. *
  1270. * const bind = Template.bind( observableInstance, emitterInstance );
  1271. *
  1272. * new Template( {
  1273. * tag: 'p',
  1274. * attributes: {
  1275. * // Plain String schema.
  1276. * class: 'static-text'
  1277. *
  1278. * // Object schema, an `ObservableMixin` binding.
  1279. * class: bind.to( 'foo' )
  1280. *
  1281. * // Array schema, combines the above.
  1282. * class: [
  1283. * 'static-text',
  1284. * bind.to( 'bar', () => { ... } )
  1285. * ],
  1286. *
  1287. * // Array schema, with custom namespace.
  1288. * class: {
  1289. * ns: 'http://ns.url',
  1290. * value: [
  1291. * bind.if( 'baz', 'value-when-true' )
  1292. * 'static-text'
  1293. * ]
  1294. * },
  1295. *
  1296. * // Object literal schema, specific for styles.
  1297. * style: {
  1298. * color: 'red',
  1299. * backgroundColor: bind.to( 'qux', () => { ... } )
  1300. * }
  1301. * }
  1302. * } );
  1303. *
  1304. * @typedef module:ui/template~TemplateValueSchema
  1305. * @type {Object|String|Array}
  1306. */
  1307. /**
  1308. * Describes a listener attached to HTMLElement. See: {@link module:ui/template~TemplateDefinition}.
  1309. *
  1310. * new Template( {
  1311. * tag: 'p',
  1312. * on: {
  1313. * // Plain String schema.
  1314. * click: 'clicked'
  1315. *
  1316. * // Object schema, an `ObservableMixin` binding.
  1317. * click: {@link module:ui/template~TemplateBinding}
  1318. *
  1319. * // Array schema, combines the above.
  1320. * click: [
  1321. * 'clicked',
  1322. * {@link module:ui/template~TemplateBinding}
  1323. * ],
  1324. *
  1325. * // Array schema, with custom callback.
  1326. * // Note: It will work for "click" event on class=".foo" children only.
  1327. * 'click@.foo': {
  1328. * 'clicked',
  1329. * {@link module:ui/template~TemplateBinding},
  1330. * () => { ... }
  1331. * }
  1332. * }
  1333. * } );
  1334. *
  1335. * @typedef module:ui/template~TemplateListenerSchema
  1336. * @type {Object|String|Array}
  1337. */
  1338. /**
  1339. * The type of {@link ~Template.bind}'s return value.
  1340. *
  1341. * @interface module:ui/template~BindChain
  1342. */
  1343. /**
  1344. * Binds {@link module:utils/observablemixin~ObservableMixin} instance to:
  1345. *
  1346. * * HTMLElement attribute or Text Node `textContent` so remains in sync with the Observable when it changes:
  1347. * * HTMLElement DOM event, so the DOM events are propagated through Observable.
  1348. *
  1349. * const bind = Template.bind( observableInstance, emitterInstance );
  1350. *
  1351. * new Template( {
  1352. * tag: 'p',
  1353. * attributes: {
  1354. * // class="..." attribute gets bound to `observableInstance#a`
  1355. * 'class': bind.to( 'a' )
  1356. * },
  1357. * children: [
  1358. * // <p>...</p> gets bound to `observableInstance#b`; always `toUpperCase()`.
  1359. * { text: bind.to( 'b', ( value, node ) => value.toUpperCase() ) }
  1360. * ],
  1361. * on: {
  1362. * click: [
  1363. * // "clicked" event will be fired on `observableInstance` when "click" fires in DOM.
  1364. * bind.to( 'clicked' ),
  1365. *
  1366. * // A custom callback function will be executed when "click" fires in DOM.
  1367. * bind.to( () => {
  1368. * ...
  1369. * } )
  1370. * ]
  1371. * }
  1372. * } ).render();
  1373. *
  1374. * const bind = Template.bind( observableInstance, emitterInstance );
  1375. *
  1376. * new Template( {
  1377. * tag: 'p',
  1378. * } ).render();
  1379. *
  1380. * @method #to
  1381. * @param {String|Function} eventNameOrFunctionOrAttribute An attribute name of
  1382. * {@link module:utils/observablemixin~ObservableMixin} or a DOM event name or an event callback.
  1383. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  1384. * @return {module:ui/template~TemplateBinding}
  1385. */
  1386. /**
  1387. * Binds {@link module:utils/observablemixin~ObservableMixin} to HTMLElement attribute or Text Node `textContent`
  1388. * so remains in sync with the Model when it changes. Unlike {@link module:ui/template~BindChain#to},
  1389. * it controls the presence of the attribute/`textContent` depending on the "falseness" of
  1390. * {@link module:utils/observablemixin~ObservableMixin} attribute.
  1391. *
  1392. * const bind = Template.bind( observableInstance, emitterInstance );
  1393. *
  1394. * new Template( {
  1395. * tag: 'input',
  1396. * attributes: {
  1397. * // <input checked> when `observableInstance#a` is not undefined/null/false/''
  1398. * // <input> when `observableInstance#a` is undefined/null/false
  1399. * checked: bind.if( 'a' )
  1400. * },
  1401. * children: [
  1402. * {
  1403. * // <input>"b-is-not-set"</input> when `observableInstance#b` is undefined/null/false/''
  1404. * // <input></input> when `observableInstance#b` is not "falsy"
  1405. * text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
  1406. * }
  1407. * ]
  1408. * } ).render();
  1409. *
  1410. * @method #if
  1411. * @param {String} attribute An attribute name of {@link module:utils/observablemixin~ObservableMixin} used in the binding.
  1412. * @param {String} [valueIfTrue] Value set when {@link module:utils/observablemixin~ObservableMixin} attribute is not
  1413. * undefined/null/false/''.
  1414. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  1415. * @return {module:ui/template~TemplateBinding}
  1416. */
  1417. /**
  1418. * The {@link module:ui/template~Template#_renderNode} configuration.
  1419. *
  1420. * @private
  1421. * @interface module:ui/template~RenderData
  1422. */
  1423. /**
  1424. * Tells {@link module:ui/template~Template#_renderNode} to render
  1425. * children into `DocumentFragment` first and then append the fragment
  1426. * to the parent element. It's a speed optimization.
  1427. *
  1428. * @member {Boolean} #intoFragment
  1429. */
  1430. /**
  1431. * A node which is being rendered.
  1432. *
  1433. * @member {HTMLElement|Text} #node
  1434. */
  1435. /**
  1436. * Indicates whether the {@module:ui/template~RenderNodeOptions#node} has
  1437. * been provided by {@module:ui/template~Template#apply}.
  1438. *
  1439. * @member {Boolean} #isApplying
  1440. */
  1441. /**
  1442. * An object storing the data that helps {@module:ui/template~Template#revert}
  1443. * bringing back an element to its initial state, i.e. before
  1444. * {@module:ui/template~Template#apply} was called.
  1445. *
  1446. * @member {Object} #revertData
  1447. */