template.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277
  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~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. // Also don't clone View instances if provided as a child of the Template. The template
  710. // instance will be extracted from the View during the normalization and there's no need
  711. // to clone it.
  712. if ( value && ( value instanceof TemplateBinding || isView( value ) || isViewCollection( value ) ) ) {
  713. return value;
  714. }
  715. } );
  716. return clone;
  717. }
  718. // Normalizes given {@link module:ui/template~TemplateDefinition}.
  719. //
  720. // See:
  721. // * {@link normalizeAttributes}
  722. // * {@link normalizeListeners}
  723. // * {@link normalizePlainTextDefinition}
  724. // * {@link normalizeTextDefinition}
  725. //
  726. // @param {module:ui/template~TemplateDefinition} def
  727. // @returns {module:ui/template~TemplateDefinition} Normalized definition.
  728. function normalize( def ) {
  729. if ( typeof def == 'string' ) {
  730. def = normalizePlainTextDefinition( def );
  731. } else if ( def.text ) {
  732. normalizeTextDefinition( def );
  733. }
  734. if ( def.on ) {
  735. def.eventListeners = normalizeListeners( def.on );
  736. // Template mixes EmitterMixin, so delete #on to avoid collision.
  737. delete def.on;
  738. }
  739. if ( !def.text ) {
  740. if ( def.attributes ) {
  741. normalizeAttributes( def.attributes );
  742. }
  743. const children = new Collection();
  744. if ( def.children ) {
  745. if ( isViewCollection( def.children ) ) {
  746. children.add( def.children );
  747. } else {
  748. for ( let child of def.children ) {
  749. if ( isView( child ) ) {
  750. children.add( child );
  751. } else {
  752. children.add( new Template( child ) );
  753. }
  754. }
  755. }
  756. }
  757. def.children = children;
  758. }
  759. return def;
  760. }
  761. // Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
  762. //
  763. // attributes: {
  764. // a: 'bar',
  765. // b: {@link module:ui/template~TemplateBinding},
  766. // c: {
  767. // value: 'bar'
  768. // }
  769. // }
  770. //
  771. // becomes
  772. //
  773. // attributes: {
  774. // a: [ 'bar' ],
  775. // b: [ {@link module:ui/template~TemplateBinding} ],
  776. // c: {
  777. // value: [ 'bar' ]
  778. // }
  779. // }
  780. //
  781. // @param {Object} attrs
  782. function normalizeAttributes( attrs ) {
  783. for ( let a in attrs ) {
  784. if ( attrs[ a ].value ) {
  785. attrs[ a ].value = [].concat( attrs[ a ].value );
  786. }
  787. arrayify( attrs, a );
  788. }
  789. }
  790. // Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
  791. //
  792. // on: {
  793. // a: 'bar',
  794. // b: {@link module:ui/template~TemplateBinding},
  795. // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
  796. // }
  797. //
  798. // becomes
  799. //
  800. // on: {
  801. // a: [ 'bar' ],
  802. // b: [ {@link module:ui/template~TemplateBinding} ],
  803. // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
  804. // }
  805. //
  806. // @param {Object} listeners
  807. // @returns {Object} Object containing normalized listeners.
  808. function normalizeListeners( listeners ) {
  809. for ( let l in listeners ) {
  810. arrayify( listeners, l );
  811. }
  812. return listeners;
  813. }
  814. // Normalizes "string" {@link module:ui/template~TemplateDefinition}.
  815. //
  816. // "foo"
  817. //
  818. // becomes
  819. //
  820. // { text: [ 'foo' ] },
  821. //
  822. // @param {String} def
  823. // @returns {module:ui/template~TemplateDefinition} Normalized template definition.
  824. function normalizePlainTextDefinition( def ) {
  825. return {
  826. text: [ def ]
  827. };
  828. }
  829. // Normalizes text {@link module:ui/template~TemplateDefinition}.
  830. //
  831. // children: [
  832. // { text: 'def' },
  833. // { text: {@link module:ui/template~TemplateBinding} }
  834. // ]
  835. //
  836. // becomes
  837. //
  838. // children: [
  839. // { text: [ 'def' ] },
  840. // { text: [ {@link module:ui/template~TemplateBinding} ] }
  841. // ]
  842. //
  843. // @param {module:ui/template~TemplateDefinition} def
  844. function normalizeTextDefinition( def ) {
  845. if ( !Array.isArray( def.text ) ) {
  846. def.text = [ def.text ];
  847. }
  848. }
  849. // Wraps an entry in Object in an Array, if not already one.
  850. //
  851. // {
  852. // x: 'y',
  853. // a: [ 'b' ]
  854. // }
  855. //
  856. // becomes
  857. //
  858. // {
  859. // x: [ 'y' ],
  860. // a: [ 'b' ]
  861. // }
  862. //
  863. // @param {Object} obj
  864. // @param {String} key
  865. function arrayify( obj, key ) {
  866. if ( !Array.isArray( obj[ key ] ) ) {
  867. obj[ key ] = [ obj[ key ] ];
  868. }
  869. }
  870. // A helper which concatenates the value avoiding unwanted
  871. // leading white spaces.
  872. //
  873. // @param {String} prev
  874. // @param {String} cur
  875. // @returns {String}
  876. function arrayValueReducer( prev, cur ) {
  877. if ( isFalsy( cur ) ) {
  878. return prev;
  879. } else if ( isFalsy( prev ) ) {
  880. return cur;
  881. } else {
  882. return `${prev} ${cur}`;
  883. }
  884. }
  885. // Extends one object defined in the following format:
  886. //
  887. // {
  888. // key1: [Array1],
  889. // key2: [Array2],
  890. // ...
  891. // keyN: [ArrayN]
  892. // }
  893. //
  894. // with another object of the same data format.
  895. //
  896. // @param {Object} obj Base object.
  897. // @param {Object} ext Object extending base.
  898. // @returns {String}
  899. function extendObjectValueArray( obj, ext ) {
  900. for ( let a in ext ) {
  901. if ( obj[ a ] ) {
  902. obj[ a ].push( ...ext[ a ] );
  903. } else {
  904. obj[ a ] = ext[ a ];
  905. }
  906. }
  907. }
  908. // A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
  909. // with content from {module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
  910. //
  911. // @param {module:ui/template~Template} def A template instance to be extended.
  912. // @param {module:ui/template~TemplateDefinition} def A definition which is to extend the template instance.
  913. function extendTemplate( template, def ) {
  914. if ( def.attributes ) {
  915. if ( !template.attributes ) {
  916. template.attributes = {};
  917. }
  918. extendObjectValueArray( template.attributes, def.attributes );
  919. }
  920. if ( def.eventListeners ) {
  921. if ( !template.eventListeners ) {
  922. template.eventListeners = {};
  923. }
  924. extendObjectValueArray( template.eventListeners, def.eventListeners );
  925. }
  926. if ( def.text ) {
  927. template.text.push( ...def.text );
  928. }
  929. if ( def.children && def.children.length ) {
  930. if ( template.children.length != def.children.length ) {
  931. /**
  932. * The number of children in extended definition does not match.
  933. *
  934. * @error ui-template-extend-children-mismatch
  935. */
  936. throw new CKEditorError( 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.' );
  937. }
  938. let childIndex = 0;
  939. for ( let childDef of def.children ) {
  940. extendTemplate( template.children.get( childIndex++ ), childDef );
  941. }
  942. }
  943. }
  944. // Checks if value is "falsy".
  945. // Note: 0 (Number) is not "falsy" in this context.
  946. //
  947. // @private
  948. // @param {*} value Value to be checked.
  949. function isFalsy( value ) {
  950. return !value && value !== 0;
  951. }
  952. // Checks if the item is an instance of {@link module:ui/view~View}
  953. //
  954. // @private
  955. // @param {*} value Value to be checked.
  956. function isView( item ) {
  957. return item instanceof View;
  958. }
  959. // Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
  960. //
  961. // @private
  962. // @param {*} value Value to be checked.
  963. function isViewCollection( item ) {
  964. return item instanceof ViewCollection;
  965. }
  966. /**
  967. * A definition of {@link module:ui/template~Template}.
  968. * See: {@link module:ui/template~TemplateValueSchema}.
  969. *
  970. * new Template( {
  971. * tag: 'p',
  972. * children: [
  973. * {
  974. * tag: 'span',
  975. * attributes: { ... },
  976. * children: [ ... ],
  977. * ...
  978. * },
  979. * {
  980. * text: 'static–text'
  981. * },
  982. * 'also-static–text',
  983. * <{@link module:ui/view~View} instance>
  984. * ...
  985. * ],
  986. * attributes: {
  987. * class: {@link module:ui/template~TemplateValueSchema},
  988. * id: {@link module:ui/template~TemplateValueSchema},
  989. * style: {@link module:ui/template~TemplateValueSchema}
  990. * ...
  991. * },
  992. * on: {
  993. * 'click': {@link module:ui/template~TemplateListenerSchema}
  994. * 'keyup@.some-class': {@link module:ui/template~TemplateListenerSchema},
  995. * ...
  996. * }
  997. * } );
  998. *
  999. * // An entire view collection can be used as a child in the definition.
  1000. * new Template( {
  1001. * tag: 'p',
  1002. * children: <{@link module:ui/viewcollection~ViewCollection} instance>
  1003. * } );
  1004. *
  1005. * @typedef module:ui/template~TemplateDefinition
  1006. * @type Object
  1007. * @property {String} tag
  1008. * @property {Array.<module:ui/template~TemplateDefinition>} [children]
  1009. * @property {Object.<String,module:ui/template~TemplateValueSchema>} [attributes]
  1010. * @property {String|module:ui/template~TemplateValueSchema|Array.<String|module:ui/template~TemplateValueSchema>} [text]
  1011. * @property {Object.<String,module:ui/template~TemplateListenerSchema>} [on]
  1012. */
  1013. /**
  1014. * Describes a value of HTMLElement attribute or `textContent`. See:
  1015. * * {@link module:ui/template~TemplateDefinition},
  1016. * * {@link module:ui/template~Template.bind},
  1017. *
  1018. * const bind = Template.bind( observableInstance, emitterInstance );
  1019. *
  1020. * new Template( {
  1021. * tag: 'p',
  1022. * attributes: {
  1023. * // Plain String schema.
  1024. * class: 'static-text'
  1025. *
  1026. * // Object schema, an `ObservableMixin` binding.
  1027. * class: bind.to( 'foo' )
  1028. *
  1029. * // Array schema, combines the above.
  1030. * class: [
  1031. * 'static-text',
  1032. * bind.to( 'bar', () => { ... } )
  1033. * ],
  1034. *
  1035. * // Array schema, with custom namespace.
  1036. * class: {
  1037. * ns: 'http://ns.url',
  1038. * value: [
  1039. * bind.if( 'baz', 'value-when-true' )
  1040. * 'static-text'
  1041. * ]
  1042. * },
  1043. *
  1044. * // Object literal schema, specific for styles.
  1045. * style: {
  1046. * color: 'red',
  1047. * backgroundColor: bind.to( 'qux', () => { ... } )
  1048. * }
  1049. * }
  1050. * } );
  1051. *
  1052. * @typedef module:ui/template~TemplateValueSchema
  1053. * @type {Object|String|Array}
  1054. */
  1055. /**
  1056. * Describes a listener attached to HTMLElement. See: {@link module:ui/template~TemplateDefinition}.
  1057. *
  1058. * new Template( {
  1059. * tag: 'p',
  1060. * on: {
  1061. * // Plain String schema.
  1062. * click: 'clicked'
  1063. *
  1064. * // Object schema, an `ObservableMixin` binding.
  1065. * click: {@link module:ui/template~TemplateBinding}
  1066. *
  1067. * // Array schema, combines the above.
  1068. * click: [
  1069. * 'clicked',
  1070. * {@link module:ui/template~TemplateBinding}
  1071. * ],
  1072. *
  1073. * // Array schema, with custom callback.
  1074. * // Note: It will work for "click" event on class=".foo" children only.
  1075. * 'click@.foo': {
  1076. * 'clicked',
  1077. * {@link module:ui/template~TemplateBinding},
  1078. * () => { ... }
  1079. * }
  1080. * }
  1081. * } );
  1082. *
  1083. * @typedef module:ui/template~TemplateListenerSchema
  1084. * @type {Object|String|Array}
  1085. */
  1086. /**
  1087. * The type of {@link ~Template.bind}'s return value.
  1088. *
  1089. * @interface module:ui/template~BindChain
  1090. */
  1091. /**
  1092. * Binds {@link module:utils/observablemixin~ObservableMixin} instance to:
  1093. *
  1094. * * HTMLElement attribute or Text Node `textContent` so remains in sync with the Observable when it changes:
  1095. * * HTMLElement DOM event, so the DOM events are propagated through Observable.
  1096. *
  1097. * const bind = Template.bind( observableInstance, emitterInstance );
  1098. *
  1099. * new Template( {
  1100. * tag: 'p',
  1101. * attributes: {
  1102. * // class="..." attribute gets bound to `observableInstance#a`
  1103. * 'class': bind.to( 'a' )
  1104. * },
  1105. * children: [
  1106. * // <p>...</p> gets bound to `observableInstance#b`; always `toUpperCase()`.
  1107. * { text: bind.to( 'b', ( value, node ) => value.toUpperCase() ) }
  1108. * ],
  1109. * on: {
  1110. * click: [
  1111. * // "clicked" event will be fired on `observableInstance` when "click" fires in DOM.
  1112. * bind.to( 'clicked' ),
  1113. *
  1114. * // A custom callback function will be executed when "click" fires in DOM.
  1115. * bind.to( () => {
  1116. * ...
  1117. * } )
  1118. * ]
  1119. * }
  1120. * } ).render();
  1121. *
  1122. * const bind = Template.bind( observableInstance, emitterInstance );
  1123. *
  1124. * new Template( {
  1125. * tag: 'p',
  1126. * } ).render();
  1127. *
  1128. * @method #to
  1129. * @param {String|Function} eventNameOrFunctionOrAttribute An attribute name of
  1130. * {@link module:utils/observablemixin~ObservableMixin} or a DOM event name or an event callback.
  1131. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  1132. * @return {module:ui/template~TemplateBinding}
  1133. */
  1134. /**
  1135. * Binds {@link module:utils/observablemixin~ObservableMixin} to HTMLElement attribute or Text Node `textContent`
  1136. * so remains in sync with the Model when it changes. Unlike {@link module:ui/template~BindChain#to},
  1137. * it controls the presence of the attribute/`textContent` depending on the "falseness" of
  1138. * {@link module:utils/observablemixin~ObservableMixin} attribute.
  1139. *
  1140. * const bind = Template.bind( observableInstance, emitterInstance );
  1141. *
  1142. * new Template( {
  1143. * tag: 'input',
  1144. * attributes: {
  1145. * // <input checked> when `observableInstance#a` is not undefined/null/false/''
  1146. * // <input> when `observableInstance#a` is undefined/null/false
  1147. * checked: bind.if( 'a' )
  1148. * },
  1149. * children: [
  1150. * {
  1151. * // <input>"b-is-not-set"</input> when `observableInstance#b` is undefined/null/false/''
  1152. * // <input></input> when `observableInstance#b` is not "falsy"
  1153. * text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
  1154. * }
  1155. * ]
  1156. * } ).render();
  1157. *
  1158. * @method #if
  1159. * @param {String} attribute An attribute name of {@link module:utils/observablemixin~ObservableMixin} used in the binding.
  1160. * @param {String} [valueIfTrue] Value set when {@link module:utils/observablemixin~ObservableMixin} attribute is not
  1161. * undefined/null/false/''.
  1162. * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
  1163. * @return {module:ui/template~TemplateBinding}
  1164. */