8
0

template.js 49 KB

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