8
0

template.js 37 KB

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