template.js 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document */
  6. import CKEditorError from '../utils/ckeditorerror.js';
  7. import mix from '../utils/mix.js';
  8. import EmitterMixin from '../utils/emittermixin.js';
  9. import cloneDeepWith from '../utils/lib/lodash/cloneDeepWith.js';
  10. const bindToSymbol = Symbol( 'bindTo' );
  11. const bindIfSymbol = Symbol( 'bindIf' );
  12. /**
  13. * A basic Template class. It renders DOM HTMLElements from {@link ui.TemplateDefinition} and supports
  14. * element attributes, children, bindings to {@link utils.ObservableMixin} instances and DOM events
  15. * propagation. For example:
  16. *
  17. * new Template( {
  18. * tag: 'p',
  19. * attributes: {
  20. * class: 'foo',
  21. * style: {
  22. * backgroundColor: 'yellow'
  23. * }
  24. * },
  25. * children: [
  26. * 'A paragraph.'
  27. * ]
  28. * } ).render();
  29. *
  30. * will render the following HTMLElement:
  31. *
  32. * <p class="foo" style="background-color: yellow;">A paragraph.</p>
  33. *
  34. * See {@link ui.TemplateDefinition} to know more about templates and see complex examples.
  35. *
  36. * @memberOf ui
  37. */
  38. export default class Template {
  39. /**
  40. * Creates an instance of the {@link ui.Template} class.
  41. *
  42. * @param {ui.TemplateDefinition} def The definition of the template.
  43. */
  44. constructor( def ) {
  45. const defClone = clone( def );
  46. normalize( defClone );
  47. /**
  48. * Definition of this template.
  49. *
  50. * @readonly
  51. * @member {ui.TemplateDefinition} ui.Template#definition
  52. */
  53. this.definition = defClone;
  54. }
  55. /**
  56. * Renders DOM Node using {@link ui.Template#definition}.
  57. *
  58. * @see ui.Template#apply
  59. *
  60. * @returns {HTMLElement}
  61. */
  62. render() {
  63. return this._renderNode( this.definition, undefined, true );
  64. }
  65. /**
  66. * Applies template {@link ui.Template#def} to existing DOM tree.
  67. *
  68. * **Note:** No new DOM nodes (elements, text nodes) will be created.
  69. * const element = document.createElement( 'div' );
  70. * const bind = Template.bind( observableInstance, emitterInstance );
  71. *
  72. * const template = new Template( {
  73. * attrs: {
  74. * id: 'first-div',
  75. * class: bind.to( 'divClass' )
  76. * },
  77. * on: {
  78. * click: bind( 'elementClicked' ) // Will be fired by the observableInstance.
  79. * }
  80. * children: [
  81. * 'Div text.'
  82. * ]
  83. * } );
  84. *
  85. * template.apply( element );
  86. *
  87. * element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
  88. *
  89. * @see ui.Template#render
  90. * @param {Node} element Root element for template to apply.
  91. */
  92. apply( node ) {
  93. if ( !node ) {
  94. /**
  95. * No DOM Node specified.
  96. *
  97. * @error ui-template-wrong-syntax
  98. */
  99. throw new CKEditorError( 'ui-template-wrong-node: No DOM Node specified.' );
  100. }
  101. return this._renderNode( this.definition, node );
  102. }
  103. /**
  104. * An entry point to the interface which allows binding DOM nodes to {@link utils.ObservableMixin}.
  105. * There are two types of bindings:
  106. *
  107. * * `HTMLElement` attributes or Text Node `textContent` can be synchronized with {@link utils.ObservableMixin}
  108. * instance attributes. See {@link ui.Template.bind#to} and {@link ui.Template.bind#if}.
  109. *
  110. * * DOM events fired on `HTMLElement` can be propagated through {@link utils.ObservableMixin}.
  111. * See {@link ui.Template.bind#to}.
  112. *
  113. * @param {utils.ObservableMixin} observable An instance of ObservableMixin class.
  114. * @param {utils.EmitterMixin} emitter An instance of `EmitterMixin` class. It listens
  115. * to `observable` attribute changes and DOM Events, depending on the binding. Usually {@link ui.View} instance.
  116. * @returns {Object}
  117. */
  118. static bind( observable, emitter ) {
  119. return {
  120. /**
  121. * Binds {@link utils.ObservableMixin} instance to:
  122. * * HTMLElement attribute or Text Node `textContent` so remains in sync with the Observable when it changes:
  123. * * HTMLElement DOM event, so the DOM events are propagated through Observable.
  124. *
  125. * const bind = Template.bind( observableInstance, emitterInstance );
  126. *
  127. * new Template( {
  128. * tag: 'p',
  129. * attributes: {
  130. * // class="..." attribute gets bound to `observableInstance#a`
  131. * 'class': bind.to( 'a' )
  132. * },
  133. * children: [
  134. * // <p>...</p> gets bound to `observableInstance#b`; always `toUpperCase()`.
  135. * { text: bind.to( 'b', ( value, node ) => value.toUpperCase() ) }
  136. * ],
  137. * on: {
  138. * click: [
  139. * // "clicked" event will be fired on `observableInstance` when "click" fires in DOM.
  140. * bind( 'clicked' ),
  141. *
  142. * // A custom callback function will be executed when "click" fires in DOM.
  143. * bind( () => {
  144. * ...
  145. * } )
  146. * ]
  147. * }
  148. * } ).render();
  149. *
  150. * const bind = Template.bind( observableInstance, emitterInstance );
  151. *
  152. * new Template( {
  153. * tag: 'p',
  154. * } ).render();
  155. *
  156. * @static
  157. * @method ui.Template.bind#to
  158. * @param {String} attribute Name of {@link utils.ObservableMixin} used in the binding.
  159. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  160. * @return {ui.TemplateBinding}
  161. */
  162. to( eventNameOrFuncionOrAttribute, callback ) {
  163. return {
  164. type: bindToSymbol,
  165. eventNameOrFunction: eventNameOrFuncionOrAttribute,
  166. attribute: eventNameOrFuncionOrAttribute,
  167. observable, emitter, callback
  168. };
  169. },
  170. /**
  171. * Binds {@link utils.ObservableMixin} to HTMLElement attribute or Text Node `textContent`
  172. * so remains in sync with the Model when it changes. Unlike {@link ui.Template.bind#to},
  173. * it controls the presence of the attribute/`textContent` depending on the "falseness" of
  174. * {@link utils.ObservableMixin} attribute.
  175. *
  176. * const bind = Template.bind( observableInstance, emitterInstance );
  177. *
  178. * new Template( {
  179. * tag: 'input',
  180. * attributes: {
  181. * // <input checked> when `observableInstance#a` is not undefined/null/false/''
  182. * // <input> when `observableInstance#a` is undefined/null/false
  183. * checked: bind.if( 'a' )
  184. * },
  185. * children: [
  186. * {
  187. * // <input>"b-is-not-set"</input> when `observableInstance#b` is undefined/null/false/''
  188. * // <input></input> when `observableInstance#b` is not "falsy"
  189. * text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
  190. * }
  191. * ]
  192. * } ).render();
  193. *
  194. * @static
  195. * @method ui.Template.bind#if
  196. * @param {String} attribute An attribute name of {@link utils.ObservableMixin} used in the binding.
  197. * @param {String} [valueIfTrue] Value set when {@link utils.ObservableMixin} attribute is not undefined/null/false/''.
  198. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  199. * @return {ui.TemplateBinding}
  200. */
  201. if( attribute, valueIfTrue, callback ) {
  202. return {
  203. type: bindIfSymbol,
  204. observable, emitter, attribute, valueIfTrue, callback
  205. };
  206. }
  207. };
  208. }
  209. /**
  210. * Extends {@link ui.Template} or {@link ui.TemplateDefinition} with additional content.
  211. *
  212. * const bind = Template.bind( observable, emitterInstance );
  213. * const instance = new Template( {
  214. * tag: 'p',
  215. * attributes: {
  216. * class: 'a',
  217. * data-x: bind.to( 'foo' )
  218. * },
  219. * children: [
  220. * {
  221. * tag: 'span',
  222. * attributes: {
  223. * class: 'b'
  224. * },
  225. * children: [
  226. * 'Span'
  227. * ]
  228. * }
  229. * ]
  230. * } );
  231. *
  232. * // Instance-level extension.
  233. * Template.extend( instance, {
  234. * attributes: {
  235. * class: 'b',
  236. * data-x: bind.to( 'bar' )
  237. * },
  238. * children: [
  239. * {
  240. * attributes: {
  241. * class: 'c'
  242. * }
  243. * }
  244. * ]
  245. * } );
  246. *
  247. * // Fragment extension.
  248. * Template.extend( instance.definition.children[ 0 ], {
  249. * attributes: {
  250. * class: 'd'
  251. * }
  252. * } );
  253. *
  254. * the `instance.render().outerHTML` is
  255. *
  256. * <p class="a b" data-x="{ observable.foo } { observable.bar }">
  257. * <span class="b c d">Span</span>
  258. * </p>
  259. *
  260. * @param {ui.Template|ui.TemplateDefinition} instanceOrDef Existing Template instance or definition to be extended.
  261. * @param {ui.TemplateDefinition} extDef An extension to existing instance or definition.
  262. */
  263. static extend( instanceOrDef, extDef ) {
  264. const extDefClone = clone( extDef );
  265. normalize( extDefClone );
  266. if ( instanceOrDef instanceof Template ) {
  267. extendTemplateDefinition( instanceOrDef.definition, extDefClone );
  268. }
  269. // Extend a particular child in existing template instance.
  270. //
  271. // Template.extend( instance.definition.children[ 0 ], {
  272. // attributes: {
  273. // class: 'd'
  274. // }
  275. // } );
  276. //
  277. else {
  278. extendTemplateDefinition( instanceOrDef, extDefClone );
  279. }
  280. }
  281. /**
  282. * Renders a DOM Node from definition.
  283. *
  284. * @protected
  285. * @param {ui.TemplateDefinition} def Definition of a Node.
  286. * @param {Node} applyNode If specified, template `def` will be applied to existing DOM Node.
  287. * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
  288. * @returns {HTMLElement} A rendered Node.
  289. */
  290. _renderNode( def, applyNode, intoFragment ) {
  291. let isInvalid;
  292. if ( applyNode ) {
  293. // When applying, a definition cannot have "tag" and "text" at the same time.
  294. isInvalid = def.tag && def.text;
  295. } else {
  296. // When rendering, a definition must have either "tag" or "text": XOR( def.tag, def.text ).
  297. isInvalid = def.tag ? def.text : !def.text;
  298. }
  299. if ( isInvalid ) {
  300. /**
  301. * Node definition cannot have "tag" and "text" properties at the same time.
  302. * Node definition must have either "tag" or "text" when rendering new Node.
  303. *
  304. * @error ui-template-wrong-syntax
  305. */
  306. throw new CKEditorError( 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering new Node.' );
  307. }
  308. return def.text ?
  309. this._renderText( def, applyNode ) : this._renderElement( def, applyNode, intoFragment );
  310. }
  311. /**
  312. * Renders an HTMLElement from TemplateDefinition.
  313. *
  314. * @protected
  315. * @param {ui.TemplateDefinition} def Definition of an element.
  316. * @param {HTMLElement} applyElement If specified, template `def` will be applied to existing HTMLElement.
  317. * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
  318. * @returns {HTMLElement} A rendered element.
  319. */
  320. _renderElement( def, applyElement, intoFragment ) {
  321. const el = applyElement ||
  322. document.createElementNS( def.ns || 'http://www.w3.org/1999/xhtml', def.tag );
  323. this._renderElementAttributes( def, el );
  324. // Invoke children recursively.
  325. if ( intoFragment ) {
  326. const docFragment = document.createDocumentFragment();
  327. this._renderElementChildren( def, docFragment );
  328. el.appendChild( docFragment );
  329. } else {
  330. this._renderElementChildren( def, el, !!applyElement );
  331. }
  332. // Setup DOM bindings event listeners.
  333. this._setUpListeners( def, el );
  334. return el;
  335. }
  336. /**
  337. * Renders a Text from TemplateDefinition or String.
  338. *
  339. * @protected
  340. * @param {TemplateDefinition|String} def Definition of Text or its value.
  341. * @param {HTMLElement} textNode If specified, template `def` will be applied to existing Text Node.
  342. * @returns {Text} A rendered Text.
  343. */
  344. _renderText( valueSchemaOrText, textNode = document.createTextNode( '' ) ) {
  345. // Check if this Text Node is bound to Observable. Cases:
  346. // { text: [ Template.bind( ... ).to( ... ) ] }
  347. // { text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
  348. if ( hasBinding( valueSchemaOrText.text ) ) {
  349. this._bindToObservable( valueSchemaOrText.text, textNode, getTextUpdater( textNode ) );
  350. }
  351. // Simply set text. Cases:
  352. // { text: [ 'all', 'are', 'static' ] }
  353. // { text: [ 'foo' ] }
  354. else {
  355. textNode.textContent = valueSchemaOrText.text.join( '' );
  356. }
  357. return textNode;
  358. }
  359. /**
  360. * Renders element attributes from definition.
  361. *
  362. * @protected
  363. * @param {ui.TemplateDefinition} def Definition of an element.
  364. * @param {HTMLElement} el Element which is rendered.
  365. */
  366. _renderElementAttributes( { attributes }, el ) {
  367. let attrName, attrValue, attrNs;
  368. if ( !attributes ) {
  369. return;
  370. }
  371. for ( attrName in attributes ) {
  372. attrValue = attributes[ attrName ];
  373. attrNs = attrValue[ 0 ].ns || null;
  374. // Activate binding if one is found. Cases:
  375. // { class: [ Template.bind( ... ).to( ... ) ] }
  376. // { class: [ 'bar', Template.bind( ... ).to( ... ), 'baz' ] }
  377. // { class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
  378. if ( hasBinding( attrValue ) ) {
  379. // Normalize attributes with additional data like namespace:
  380. // { class: { ns: 'abc', value: [ ... ] } }
  381. this._bindToObservable(
  382. attrValue[ 0 ].value || attrValue,
  383. el,
  384. getAttributeUpdater( el, attrName, attrNs )
  385. );
  386. }
  387. // Style attribute could be an Object so it needs to be parsed in a specific way.
  388. // style: {
  389. // width: '100px',
  390. // height: Template.bind( ... ).to( ... )
  391. // }
  392. else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
  393. this._renderStyleAttribute( attrValue[ 0 ], el );
  394. }
  395. // Otherwise simply set the static attribute.
  396. // { class: [ 'foo' ] }
  397. // { class: [ 'all', 'are', 'static' ] }
  398. // { class: [ { ns: 'abc', value: [ 'foo' ] } ] }
  399. else {
  400. attrValue = attrValue
  401. // Retrieve "values" from { class: [ { ns: 'abc', value: [ ... ] } ] }
  402. .map( v => v ? ( v.value || v ) : v )
  403. // Flatten the array.
  404. .reduce( ( p, n ) => p.concat( n ), [] )
  405. // Convert into string.
  406. .reduce( arrayValueReducer );
  407. el.setAttributeNS( attrNs, attrName, attrValue );
  408. }
  409. }
  410. }
  411. /**
  412. * Renders `style` attribute.
  413. *
  414. * Style attribute is an {Object} with static values:
  415. *
  416. * attributes: {
  417. * style: {
  418. * color: 'red'
  419. * }
  420. * }
  421. *
  422. * or values bound to {@link ui.Model} properties:
  423. *
  424. * attributes: {
  425. * style: {
  426. * color: bind.to( ... )
  427. * }
  428. * }
  429. *
  430. * Note: `style` attribute is rendered without setting namespace. It does not seem to be
  431. * needed.
  432. *
  433. * @private
  434. * @param {ui.TemplateDefinition.attributes.styles} styles Styles definition.
  435. * @param {HTMLElement} el Element which is rendered.
  436. */
  437. _renderStyleAttribute( styles, el ) {
  438. for ( let styleName in styles ) {
  439. const styleValue = styles[ styleName ];
  440. // style: {
  441. // color: bind.to( 'attribute' )
  442. // }
  443. if ( hasBinding( styleValue ) ) {
  444. this._bindToObservable( [ styleValue ], el, getStyleUpdater( el, styleName ) );
  445. }
  446. // style: {
  447. // color: 'red'
  448. // }
  449. else {
  450. el.style[ styleName ] = styleValue;
  451. }
  452. }
  453. }
  454. /**
  455. * Recursively renders element children from definition by
  456. * calling {@link ui.Template#_renderElement}.
  457. *
  458. * @protected
  459. * @param {ui.TemplateDefinition} def Definition of an element.
  460. * @param {HTMLElement} el Element which is rendered.
  461. * @param {Boolean} isApply Traverse existing DOM structure only, don't modify DOM.
  462. */
  463. _renderElementChildren( def, el, isApply ) {
  464. if ( def.children ) {
  465. def.children.forEach( ( childDef, index ) => {
  466. if ( isApply ) {
  467. this._renderNode( childDef, el.childNodes[ index ] );
  468. } else {
  469. el.appendChild( this._renderNode( childDef ) );
  470. }
  471. } );
  472. }
  473. }
  474. /**
  475. * Activates element `on` listeners passed in element definition.
  476. *
  477. * @protected
  478. * @param {ui.TemplateDefinition} def Definition of an element.
  479. * @param {HTMLElement} el Element which is being rendered.
  480. */
  481. _setUpListeners( def, el ) {
  482. if ( !def.on ) {
  483. return;
  484. }
  485. for ( let key in def.on ) {
  486. const [ domEvtName, domSelector ] = key.split( '@' );
  487. for ( let schemaItem of def.on[ key ] ) {
  488. schemaItem.emitter.listenTo( el, domEvtName, ( evt, domEvt ) => {
  489. if ( !domSelector || domEvt.target.matches( domSelector ) ) {
  490. if ( typeof schemaItem.eventNameOrFunction == 'function' ) {
  491. schemaItem.eventNameOrFunction( domEvt );
  492. } else {
  493. schemaItem.observable.fire( schemaItem.eventNameOrFunction, domEvt );
  494. }
  495. }
  496. } );
  497. }
  498. }
  499. }
  500. /**
  501. * For given {@link ui.TemplateValueSchema} containing {@link ui.TemplateBinding} it activates the
  502. * binding and sets its initial value.
  503. *
  504. * Note: {@link ui.TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
  505. *
  506. * @protected
  507. * @param {ui.TemplateValueSchema} valueSchema
  508. * @param {Node} node DOM Node to be updated when {@link utils.ObservableMixin} changes.
  509. * @param {Function} domUpdater A function which updates DOM (like attribute or text).
  510. */
  511. _bindToObservable( valueSchema ) {
  512. valueSchema
  513. // Filter inactive bindings from schema, like static strings, etc.
  514. .filter( item => item.observable )
  515. // Let the emitter listen to observable change:attribute event.
  516. // TODO: Reduce the number of listeners attached as many bindings may listen
  517. // to the same observable attribute.
  518. .forEach( ( { emitter, observable, attribute } ) => {
  519. emitter.listenTo( observable, 'change:' + attribute, () => {
  520. syncBinding( ...arguments );
  521. } );
  522. } );
  523. // Set initial values.
  524. syncBinding( ...arguments );
  525. }
  526. }
  527. mix( Template, EmitterMixin );
  528. // Checks whether given {@link ui.TemplateValueSchema} contains a
  529. // {@link ui.TemplateBinding}.
  530. //
  531. // @param {ui.TemplateValueSchema} valueSchema
  532. // @returns {Boolean}
  533. function hasBinding( valueSchema ) {
  534. if ( !valueSchema ) {
  535. return false;
  536. }
  537. // Normalize attributes with additional data like namespace:
  538. // class: { ns: 'abc', value: [ ... ] }
  539. if ( valueSchema.value ) {
  540. valueSchema = valueSchema.value;
  541. }
  542. if ( Array.isArray( valueSchema ) ) {
  543. return valueSchema.some( hasBinding );
  544. } else if ( valueSchema.observable ) {
  545. return true;
  546. }
  547. return false;
  548. }
  549. // Assembles the value using {@link ui.TemplateValueSchema} and stores it in a form of
  550. // an Array. Each entry of an Array corresponds to one of {@link ui.TemplateValueSchema}
  551. // items.
  552. //
  553. // @param {ui.TemplateValueSchema} valueSchema
  554. // @param {Node} node DOM Node updated when {@link utils.ObservableMixin} changes.
  555. // @return {Array}
  556. function getBindingValue( valueSchema, domNode ) {
  557. return valueSchema.map( schemaItem => {
  558. let { observable, callback, type } = schemaItem;
  559. if ( observable ) {
  560. let modelValue = observable[ schemaItem.attribute ];
  561. // Process the value with the callback.
  562. if ( callback ) {
  563. modelValue = callback( modelValue, domNode );
  564. }
  565. if ( type === bindIfSymbol ) {
  566. return !!modelValue ? schemaItem.valueIfTrue || true : '';
  567. } else {
  568. return modelValue;
  569. }
  570. } else {
  571. return schemaItem;
  572. }
  573. } );
  574. }
  575. // A function executed each time bound Observable attribute changes, which updates DOM with a value
  576. // constructed from {@link ui.TemplateValueSchema}.
  577. //
  578. // @param {ui.TemplateValueSchema} valueSchema
  579. // @param {Node} node DOM Node updated when {@link utils.ObservableMixin} changes.
  580. // @param {Function} domUpdater A function which updates DOM (like attribute or text).
  581. function syncBinding( valueSchema, domNode, domUpdater ) {
  582. let value = getBindingValue( valueSchema, domNode );
  583. let shouldSet;
  584. // Check if valueSchema is a single Template.bind.if, like:
  585. // { class: Template.bind.if( 'foo' ) }
  586. if ( valueSchema.length == 1 && valueSchema[ 0 ].type == bindIfSymbol ) {
  587. value = value[ 0 ];
  588. shouldSet = value !== '';
  589. if ( shouldSet ) {
  590. value = value === true ? '' : value;
  591. }
  592. } else {
  593. value = value.reduce( arrayValueReducer, '' );
  594. shouldSet = value;
  595. }
  596. if ( shouldSet ) {
  597. domUpdater.set( value );
  598. } else {
  599. domUpdater.remove();
  600. }
  601. }
  602. // Returns an object consisting of `set` and `remove` functions, which
  603. // can be used in the context of DOM Node to set or reset `textContent`.
  604. // @see ui.View#_bindToObservable
  605. //
  606. // @param {Node} node DOM Node to be modified.
  607. // @returns {Object}
  608. function getTextUpdater( node ) {
  609. return {
  610. set( value ) {
  611. node.textContent = value;
  612. },
  613. remove() {
  614. node.textContent = '';
  615. }
  616. };
  617. }
  618. // Returns an object consisting of `set` and `remove` functions, which
  619. // can be used in the context of DOM Node to set or reset an attribute.
  620. // @see ui.View#_bindToObservable
  621. //
  622. // @param {Node} node DOM Node to be modified.
  623. // @param {String} attrName Name of the attribute to be modified.
  624. // @param {String} [ns=null] Namespace to use.
  625. // @returns {Object}
  626. function getAttributeUpdater( el, attrName, ns = null ) {
  627. return {
  628. set( value ) {
  629. el.setAttributeNS( ns, attrName, value );
  630. },
  631. remove() {
  632. el.removeAttributeNS( ns, attrName );
  633. }
  634. };
  635. }
  636. // Returns an object consisting of `set` and `remove` functions, which
  637. // can be used in the context of CSSStyleDeclaration to set or remove a style.
  638. // @see ui.View#_bindToObservable
  639. //
  640. // @param {Node} node DOM Node to be modified.
  641. // @param {String} styleName Name of the style to be modified.
  642. // @returns {Object}
  643. function getStyleUpdater( el, styleName ) {
  644. return {
  645. set( value ) {
  646. el.style[ styleName ] = value;
  647. },
  648. remove() {
  649. el.style[ styleName ] = null;
  650. }
  651. };
  652. }
  653. // Clones definition of the template.
  654. //
  655. // @param {ui.TemplateDefinition} def
  656. // @returns {ui.TemplateDefinition}
  657. function clone( def ) {
  658. const clone = cloneDeepWith( def, value => {
  659. // Don't clone Template.bind* bindings because there are references
  660. // to Observable and DOMEmitterMixin instances inside, which are external
  661. // to the Template.
  662. if ( value && value.type ) {
  663. return value;
  664. }
  665. } );
  666. return clone;
  667. }
  668. // Normalizes given {@link ui.TemplateDefinition}.
  669. //
  670. // See:
  671. // * {@link normalizeAttributes}
  672. // * {@link normalizeListeners}
  673. // * {@link normalizeTextString}
  674. // * {@link normalizeTextDefinition}
  675. //
  676. // @param {ui.TemplateDefinition} def
  677. function normalize( def ) {
  678. if ( def.text ) {
  679. normalizeTextDefinition( def );
  680. }
  681. if ( def.attributes ) {
  682. normalizeAttributes( def.attributes );
  683. }
  684. if ( def.on ) {
  685. normalizeListeners( def.on );
  686. }
  687. if ( def.children ) {
  688. // Splicing children array inside so no forEach.
  689. for ( let i = def.children.length; i--; ) {
  690. normalizeTextString( def.children, def.children[ i ], i );
  691. normalize( def.children[ i ] );
  692. }
  693. }
  694. }
  695. // Normalizes "attributes" section of {@link ui.TemplateDefinition}.
  696. //
  697. // attributes: {
  698. // a: 'bar',
  699. // b: {@link ui.TemplateBinding},
  700. // c: {
  701. // value: 'bar'
  702. // }
  703. // }
  704. //
  705. // becomes
  706. //
  707. // attributes: {
  708. // a: [ 'bar' ],
  709. // b: [ {@link ui.TemplateBinding} ],
  710. // c: {
  711. // value: [ 'bar' ]
  712. // }
  713. // }
  714. //
  715. // @param {Object} attrs
  716. function normalizeAttributes( attrs ) {
  717. for ( let a in attrs ) {
  718. if ( attrs[ a ].value ) {
  719. attrs[ a ].value = [].concat( attrs[ a ].value );
  720. }
  721. arrayify( attrs, a );
  722. }
  723. }
  724. // Normalizes "on" section of {@link ui.TemplateDefinition}.
  725. //
  726. // on: {
  727. // a: 'bar',
  728. // b: {@link ui.TemplateBinding},
  729. // c: [ {@link ui.TemplateBinding}, () => { ... } ]
  730. // }
  731. //
  732. // becomes
  733. //
  734. // on: {
  735. // a: [ 'bar' ],
  736. // b: [ {@link ui.TemplateBinding} ],
  737. // c: [ {@link ui.TemplateBinding}, () => { ... } ]
  738. // }
  739. //
  740. // @param {Object} listeners
  741. function normalizeListeners( listeners ) {
  742. for ( let l in listeners ) {
  743. arrayify( listeners, l );
  744. }
  745. }
  746. // Normalizes "string" text in "children" section of {@link ui.TemplateDefinition}.
  747. //
  748. // children: [
  749. // 'abc',
  750. // ]
  751. //
  752. // becomes
  753. //
  754. // children: [
  755. // { text: [ 'abc' ] },
  756. // ]
  757. //
  758. // @param {Array} children
  759. // @param {ui.TemplateDefinition} child
  760. // @param {Number} index
  761. function normalizeTextString( children, child, index ) {
  762. if ( typeof child == 'string' ) {
  763. children.splice( index, 1, {
  764. text: [ child ]
  765. } );
  766. }
  767. }
  768. // Normalizes text {@link ui.TemplateDefinition}.
  769. //
  770. // children: [
  771. // { text: 'def' },
  772. // { text: {@link ui.TemplateBinding} }
  773. // ]
  774. //
  775. // becomes
  776. //
  777. // children: [
  778. // { text: [ 'def' ] },
  779. // { text: [ {@link ui.TemplateBinding} ] }
  780. // ]
  781. //
  782. // @param {ui.TemplateDefinition} def
  783. function normalizeTextDefinition( def ) {
  784. if ( !Array.isArray( def.text ) ) {
  785. def.text = [ def.text ];
  786. }
  787. }
  788. // Wraps an entry in Object in an Array, if not already one.
  789. //
  790. // {
  791. // x: 'y',
  792. // a: [ 'b' ]
  793. // }
  794. //
  795. // becomes
  796. //
  797. // {
  798. // x: [ 'y' ],
  799. // a: [ 'b' ]
  800. // }
  801. //
  802. // @param {Object} obj
  803. // @param {String} key
  804. function arrayify( obj, key ) {
  805. if ( !Array.isArray( obj[ key ] ) ) {
  806. obj[ key ] = [ obj[ key ] ];
  807. }
  808. }
  809. // A helper which concatenates the value avoiding unwanted
  810. // leading white spaces.
  811. //
  812. // @param {String} prev
  813. // @param {String} cur
  814. // @returns {String}
  815. function arrayValueReducer( prev, cur ) {
  816. return prev === '' ?
  817. `${cur}`
  818. :
  819. cur === '' ? `${prev}` : `${prev} ${cur}`;
  820. }
  821. // Extends one object defined in the following format:
  822. //
  823. // {
  824. // key1: [Array1],
  825. // key2: [Array2],
  826. // ...
  827. // keyN: [ArrayN]
  828. // }
  829. //
  830. // with another object of the same data format.
  831. //
  832. // @param {Object} obj Base object.
  833. // @param {Object} ext Object extending base.
  834. // @returns {String}
  835. function extendObjectValueArray( obj, ext ) {
  836. for ( let a in ext ) {
  837. if ( obj[ a ] ) {
  838. obj[ a ].push( ...ext[ a ] );
  839. } else {
  840. obj[ a ] = ext[ a ];
  841. }
  842. }
  843. }
  844. // A helper for {@link ui.Template#extend}. Recursively extends {@link ui.Template#definition}
  845. // with content from another definition. See {@link ui.Template#extend} to learn more.
  846. //
  847. // @param {ui.TemplateDefinition} def A base template definition.
  848. // @param {ui.TemplateDefinition} extDef An extension to existing definition.
  849. function extendTemplateDefinition( def, extDef ) {
  850. if ( extDef.attributes ) {
  851. if ( !def.attributes ) {
  852. def.attributes = {};
  853. }
  854. extendObjectValueArray( def.attributes, extDef.attributes );
  855. }
  856. if ( extDef.on ) {
  857. if ( !def.on ) {
  858. def.on = {};
  859. }
  860. extendObjectValueArray( def.on, extDef.on );
  861. }
  862. if ( extDef.text ) {
  863. def.text.push( ...extDef.text );
  864. }
  865. if ( extDef.children ) {
  866. if ( !def.children || def.children.length != extDef.children.length ) {
  867. /**
  868. * The number of children in extended definition does not match.
  869. *
  870. * @error ui-template-extend-children-mismatch
  871. */
  872. throw new CKEditorError( 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.' );
  873. }
  874. extDef.children.forEach( ( extChildDef, index ) => {
  875. extendTemplateDefinition( def.children[ index ], extChildDef );
  876. } );
  877. }
  878. }
  879. /**
  880. * A definition of {@link ui.Template}.
  881. * See: {@link ui.TemplateValueSchema}.
  882. *
  883. * new Template( {
  884. * tag: 'p',
  885. * children: [
  886. * {
  887. * tag: 'span',
  888. * attributes: { ... },
  889. * children: [ ... ],
  890. * ...
  891. * },
  892. * {
  893. * text: 'static–text'
  894. * },
  895. * 'also-static–text',
  896. * ...
  897. * ],
  898. * attributes: {
  899. * class: {@link ui.TemplateValueSchema},
  900. * id: {@link ui.TemplateValueSchema},
  901. * style: {@link ui.TemplateValueSchema}
  902. * ...
  903. * },
  904. * on: {
  905. * 'click': {@link ui.TemplateListenerSchema}
  906. * 'keyup@.some-class': {@link ui.TemplateListenerSchema},
  907. * ...
  908. * }
  909. * } );
  910. *
  911. * @typedef ui.TemplateDefinition
  912. * @type Object
  913. * @property {String} tag
  914. * @property {Array.<ui.TemplateDefinition>} [children]
  915. * @property {Object.<String,ui.TemplateValueSchema>} [attributes]
  916. * @property {String|ui.TemplateValueSchema} [text]
  917. * @property {Object.<String,ui.TemplateListenerSchema>} [on]
  918. */
  919. /**
  920. * Describes a value of HTMLElement attribute or `textContent`. See:
  921. * * {@link ui.TemplateDefinition},
  922. * * {@link ui.Template#bind},
  923. *
  924. * const bind = Template.bind( observableInstance, emitterInstance );
  925. *
  926. * new Template( {
  927. * tag: 'p',
  928. * attributes: {
  929. * // Plain String schema.
  930. * class: 'static-text'
  931. *
  932. * // Object schema, an `ObservableMixin` binding.
  933. * class: bind.to( 'foo' )
  934. *
  935. * // Array schema, combines the above.
  936. * class: [
  937. * 'static-text',
  938. * bind.to( 'bar', () => { ... } )
  939. * ],
  940. *
  941. * // Array schema, with custom namespace.
  942. * class: {
  943. * ns: 'http://ns.url',
  944. * value: [
  945. * bind.if( 'baz', 'value-when-true' )
  946. * 'static-text'
  947. * ]
  948. * },
  949. *
  950. * // Object literal schema, specific for styles.
  951. * style: {
  952. * color: 'red',
  953. * backgroundColor: bind.to( 'qux', () => { ... } )
  954. * }
  955. * }
  956. * } );
  957. *
  958. * @typedef ui.TemplateValueSchema
  959. * @type {Object|String|Array}
  960. */
  961. /**
  962. * Describes a listener attached to HTMLElement. See: {@link ui.TemplateDefinition}.
  963. *
  964. * new Template( {
  965. * tag: 'p',
  966. * on: {
  967. * // Plain String schema.
  968. * click: 'clicked'
  969. *
  970. * // Object schema, an `ObservableMixin` binding.
  971. * click: {@link ui.TemplateBinding}
  972. *
  973. * // Array schema, combines the above.
  974. * click: [
  975. * 'clicked',
  976. * {@link ui.TemplateBinding}
  977. * ],
  978. *
  979. * // Array schema, with custom callback.
  980. * // Note: It will work for "click" event on class=".foo" children only.
  981. * 'click@.foo': {
  982. * 'clicked',
  983. * {@link ui.TemplateBinding},
  984. * () => { ... }
  985. * }
  986. * }
  987. * } );
  988. *
  989. * @typedef ui.TemplateListenerSchema
  990. * @type {Object|String|Array}
  991. */
  992. /**
  993. * Describes Model binding created via {@link ui.Template#bind}.
  994. *
  995. * @typedef ui.TemplateBinding
  996. * @type Object
  997. * @property {utils.ObservableMixin} observable
  998. * @property {utils.EmitterMixin} emitter
  999. * @property {Symbol} type
  1000. * @property {String} attribute
  1001. * @property {String} [valueIfTrue]
  1002. * @property {Function} [callback]
  1003. */