template.js 49 KB

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