template.js 37 KB

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