template.js 49 KB

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