template.js 41 KB

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