8
0

template.js 49 KB

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