template.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: ui */
  6. /* global HTMLElement */
  7. 'use strict';
  8. import testUtils from '/tests/ckeditor5/_utils/utils.js';
  9. import Template from '/ckeditor5/ui/template.js';
  10. import Model from '/ckeditor5/ui/model.js';
  11. import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
  12. import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
  13. import DOMEmitterMixin from '/ckeditor5/ui/domemittermixin.js';
  14. testUtils.createSinonSandbox();
  15. let el, text;
  16. describe( 'Template', () => {
  17. describe( 'constructor', () => {
  18. it( 'accepts template definition', () => {
  19. const def = {
  20. tag: 'p'
  21. };
  22. expect( new Template( def ).definition ).to.not.equal( def );
  23. expect( new Template( def ).definition.tag ).to.equal( 'p' );
  24. } );
  25. it( 'normalizes template definition', () => {
  26. const bind = Template.bind( new Model( {} ), Object.create( DOMEmitterMixin ) );
  27. const tpl = new Template( {
  28. tag: 'p',
  29. attributes: {
  30. a: 'foo',
  31. b: [ 'bar', 'baz' ],
  32. c: {
  33. ns: 'abc',
  34. value: bind.to( 'qux' )
  35. }
  36. },
  37. children: [
  38. {
  39. text: 'content'
  40. },
  41. {
  42. text: bind.to( 'x' )
  43. },
  44. 'abc',
  45. {
  46. text: [ 'a', 'b' ]
  47. }
  48. ],
  49. on: {
  50. 'a@span': bind.to( 'b' ),
  51. 'b@span': bind.to( () => {} ),
  52. 'c@span': [
  53. bind.to( 'c' ),
  54. bind.to( () => {} )
  55. ]
  56. }
  57. } );
  58. const def = tpl.definition;
  59. expect( def.attributes.a[ 0 ] ).to.equal( 'foo' );
  60. expect( def.attributes.b[ 0 ] ).to.equal( 'bar' );
  61. expect( def.attributes.b[ 1 ] ).to.equal( 'baz' );
  62. expect( def.attributes.c[ 0 ].value[ 0 ].type ).to.be.a( 'symbol' );
  63. expect( def.children[ 0 ].text[ 0 ] ).to.equal( 'content' );
  64. expect( def.children[ 1 ].text[ 0 ].type ).to.be.a( 'symbol' );
  65. expect( def.children[ 2 ].text[ 0 ] ).to.equal( 'abc' );
  66. expect( def.children[ 3 ].text[ 0 ] ).to.equal( 'a' );
  67. expect( def.children[ 3 ].text[ 1 ] ).to.equal( 'b' );
  68. expect( def.on[ 'a@span' ][ 0 ].type ).to.be.a( 'symbol' );
  69. expect( def.on[ 'b@span' ][ 0 ].type ).to.be.a( 'symbol' );
  70. expect( def.on[ 'c@span' ][ 0 ].type ).to.be.a( 'symbol' );
  71. expect( def.on[ 'c@span' ][ 1 ].type ).to.be.a( 'symbol' );
  72. } );
  73. it( 'does not modify passed definition', () => {
  74. const def = {
  75. tag: 'p',
  76. attributes: {
  77. a: 'foo',
  78. },
  79. children: [
  80. {
  81. tag: 'span'
  82. }
  83. ]
  84. };
  85. const tpl = new Template( def );
  86. expect( def ).to.not.equal( tpl.definition );
  87. expect( def.attributes ).to.not.equal( tpl.definition.attributes );
  88. expect( def.children ).to.not.equal( tpl.definition.children );
  89. expect( def.children[ 0 ] ).to.not.equal( tpl.definition.children[ 0 ] );
  90. expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
  91. expect( def.attributes.a ).to.equal( 'foo' );
  92. } );
  93. } );
  94. describe( 'render', () => {
  95. it( 'throws when wrong template definition', () => {
  96. expect( () => {
  97. new Template( {} ).render();
  98. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  99. expect( () => {
  100. new Template( {
  101. tag: 'p',
  102. text: 'foo'
  103. } ).render();
  104. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  105. } );
  106. it( 'creates HTMLElement', () => {
  107. const el = new Template( {
  108. tag: 'p',
  109. } ).render();
  110. expect( el ).to.be.instanceof( HTMLElement );
  111. expect( el.parentNode ).to.be.null;
  112. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  113. expect( el.namespaceURI ).to.be.equal( 'http://www.w3.org/1999/xhtml' );
  114. } );
  115. it( 'creates an element in a custom namespace', () => {
  116. const el = new Template( {
  117. tag: 'p',
  118. ns: 'foo'
  119. } ).render();
  120. expect( el.namespaceURI ).to.be.equal( 'foo' );
  121. } );
  122. it( 'renders HTMLElement attributes', () => {
  123. const el = new Template( {
  124. tag: 'p',
  125. attributes: {
  126. 'class': [ 'a', 'b' ],
  127. x: 'bar'
  128. },
  129. children: [ 'foo' ]
  130. } ).render();
  131. expect( el ).to.be.instanceof( HTMLElement );
  132. expect( el.parentNode ).to.be.null;
  133. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  134. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  135. } );
  136. it( 'renders HTMLElement attributes – empty', () => {
  137. const el = new Template( {
  138. tag: 'p',
  139. attributes: {
  140. class: '',
  141. x: [ '', '' ]
  142. },
  143. children: [ 'foo' ]
  144. } ).render();
  145. expect( el.outerHTML ).to.be.equal( '<p class="" x="">foo</p>' );
  146. } );
  147. it( 'renders HTMLElement attributes – falsy values', () => {
  148. const el = new Template( {
  149. tag: 'p',
  150. attributes: {
  151. class: false,
  152. x: [ '', null ]
  153. },
  154. children: [ 'foo' ]
  155. } ).render();
  156. expect( el.outerHTML ).to.be.equal( '<p class="false" x="null">foo</p>' );
  157. } );
  158. it( 'renders HTMLElement attributes in a custom namespace', () => {
  159. const el = new Template( {
  160. tag: 'p',
  161. attributes: {
  162. class: {
  163. ns: 'foo',
  164. value: [ 'a', 'b' ]
  165. },
  166. x: {
  167. ns: 'abc',
  168. value: 'bar'
  169. }
  170. },
  171. children: [ 'foo' ]
  172. } ).render();
  173. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  174. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  175. expect( el.attributes.getNamedItem( 'x' ).namespaceURI ).to.equal( 'abc' );
  176. } );
  177. it( 'creates HTMLElement children', () => {
  178. const el = new Template( {
  179. tag: 'p',
  180. attributes: {
  181. a: 'A'
  182. },
  183. children: [
  184. {
  185. tag: 'b',
  186. children: [ 'B' ]
  187. },
  188. {
  189. tag: 'i',
  190. children: [
  191. 'C',
  192. {
  193. tag: 'b',
  194. children: [ 'D' ]
  195. }
  196. ]
  197. }
  198. ]
  199. } ).render();
  200. expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
  201. } );
  202. it( 'creates a Text node', () => {
  203. const node = new Template( { text: 'foo' } ).render();
  204. expect( node.nodeType ).to.be.equal( 3 );
  205. expect( node.textContent ).to.be.equal( 'foo' );
  206. } );
  207. it( 'creates a child Text Node (different syntaxes)', () => {
  208. const el = new Template( {
  209. tag: 'p',
  210. children: [
  211. 'foo',
  212. { text: 'bar' }
  213. ]
  214. } ).render();
  215. expect( el.outerHTML ).to.be.equal( '<p>foobar</p>' );
  216. } );
  217. it( 'creates multiple child Text Nodes', () => {
  218. const el = new Template( {
  219. tag: 'p',
  220. children: [
  221. 'a',
  222. 'b',
  223. { text: 'c' },
  224. 'd',
  225. { text: [ 'e', 'f' ] }
  226. ]
  227. } ).render();
  228. expect( el.childNodes ).to.have.length( 5 );
  229. expect( el.outerHTML ).to.be.equal( '<p>abcdef</p>' );
  230. } );
  231. it( 'activates model bindings – root', () => {
  232. const observable = new Model( {
  233. foo: 'bar'
  234. } );
  235. const emitter = Object.create( EmitterMixin );
  236. const bind = Template.bind( observable, emitter );
  237. const el = new Template( {
  238. tag: 'div',
  239. attributes: {
  240. class: bind.to( 'foo' )
  241. }
  242. } ).render();
  243. expect( el.getAttribute( 'class' ) ).to.equal( 'bar' );
  244. observable.foo = 'baz';
  245. expect( el.getAttribute( 'class' ) ).to.equal( 'baz' );
  246. } );
  247. it( 'activates model bindings – children', () => {
  248. const observable = new Model( {
  249. foo: 'bar'
  250. } );
  251. const emitter = Object.create( EmitterMixin );
  252. const bind = Template.bind( observable, emitter );
  253. const el = new Template( {
  254. tag: 'div',
  255. children: [
  256. {
  257. tag: 'span',
  258. children: [
  259. {
  260. text: [
  261. bind.to( 'foo' ),
  262. 'static'
  263. ]
  264. }
  265. ]
  266. }
  267. ]
  268. } ).render();
  269. expect( el.firstChild.textContent ).to.equal( 'bar static' );
  270. observable.foo = 'baz';
  271. expect( el.firstChild.textContent ).to.equal( 'baz static' );
  272. } );
  273. describe( '`style` attribute', () => {
  274. let observable, emitter, bind;
  275. beforeEach( () => {
  276. observable = new Model( {
  277. width: '10px',
  278. backgroundColor: 'yellow'
  279. } );
  280. emitter = Object.create( EmitterMixin );
  281. bind = Template.bind( observable, emitter );
  282. } );
  283. it( 'renders with static and bound properties', () => {
  284. setElement( {
  285. tag: 'p',
  286. attributes: {
  287. style: {
  288. width: bind.to( 'width' ),
  289. height: '10px',
  290. backgroundColor: bind.to( 'backgroundColor' )
  291. }
  292. }
  293. } );
  294. expect( el.outerHTML ).to.equal( '<p style="width: 10px; height: 10px; background-color: yellow;"></p>' );
  295. observable.width = '20px';
  296. observable.backgroundColor = 'green';
  297. expect( el.outerHTML ).to.equal( '<p style="width: 20px; height: 10px; background-color: green;"></p>' );
  298. } );
  299. it( 'renders with empty properties', () => {
  300. setElement( {
  301. tag: 'p',
  302. attributes: {
  303. style: {
  304. width: '',
  305. backgroundColor: bind.to( 'backgroundColor' )
  306. }
  307. }
  308. } );
  309. observable.backgroundColor = '';
  310. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  311. } );
  312. it( 'renders with falsy values', () => {
  313. setElement( {
  314. tag: 'p',
  315. attributes: {
  316. style: {
  317. width: null,
  318. height: false
  319. }
  320. }
  321. } );
  322. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  323. } );
  324. it( 'renders with empty properties', () => {
  325. setElement( {
  326. tag: 'p',
  327. attributes: {
  328. style: {
  329. width: '',
  330. backgroundColor: bind.to( 'backgroundColor' )
  331. }
  332. }
  333. } );
  334. observable.backgroundColor = '';
  335. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  336. } );
  337. } );
  338. } );
  339. describe( 'apply', () => {
  340. let observable, domEmitter, bind;
  341. beforeEach( () => {
  342. el = document.createElement( 'div' );
  343. text = document.createTextNode( '' );
  344. observable = new Model( {
  345. foo: 'bar',
  346. baz: 'qux'
  347. } );
  348. domEmitter = Object.create( DOMEmitterMixin );
  349. bind = Template.bind( observable, domEmitter );
  350. } );
  351. it( 'throws when wrong template definition', () => {
  352. expect( () => {
  353. new Template( {
  354. tag: 'p',
  355. text: 'foo'
  356. } ).apply( el );
  357. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  358. } );
  359. it( 'throws when no HTMLElement passed', () => {
  360. expect( () => {
  361. new Template( {
  362. tag: 'p'
  363. } ).apply();
  364. } ).to.throw( CKEditorError, /ui-template-wrong-node/ );
  365. } );
  366. it( 'accepts empty template definition', () => {
  367. new Template( {} ).apply( el );
  368. new Template( {} ).apply( text );
  369. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  370. expect( text.textContent ).to.be.equal( '' );
  371. } );
  372. it( 'applies textContent to a Text Node', () => {
  373. new Template( {
  374. text: 'abc'
  375. } ).apply( text );
  376. expect( text.textContent ).to.be.equal( 'abc' );
  377. } );
  378. it( 'applies attributes to an HTMLElement', () => {
  379. new Template( {
  380. tag: 'div',
  381. attributes: {
  382. 'class': [ 'a', 'b' ],
  383. x: 'bar'
  384. }
  385. } ).apply( el );
  386. expect( el.outerHTML ).to.be.equal( '<div class="a b" x="bar"></div>' );
  387. } );
  388. it( 'doesn\'t apply new child to an HTMLElement – Text Node', () => {
  389. new Template( {
  390. tag: 'div',
  391. children: [ 'foo' ]
  392. } ).apply( el );
  393. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  394. } );
  395. it( 'doesn\'t apply new child to an HTMLElement – HTMLElement', () => {
  396. new Template( {
  397. tag: 'div',
  398. children: [
  399. {
  400. tag: 'span'
  401. }
  402. ]
  403. } ).apply( el );
  404. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  405. } );
  406. it( 'applies new textContent to an existing Text Node of an HTMLElement', () => {
  407. el.textContent = 'bar';
  408. new Template( {
  409. tag: 'div',
  410. children: [ 'foo' ]
  411. } ).apply( el );
  412. expect( el.outerHTML ).to.be.equal( '<div>foo</div>' );
  413. } );
  414. it( 'applies attributes and TextContent to a DOM tree', () => {
  415. el.textContent = 'abc';
  416. el.appendChild( document.createElement( 'span' ) );
  417. new Template( {
  418. tag: 'div',
  419. attributes: {
  420. 'class': 'parent'
  421. },
  422. children: [
  423. 'Children: ',
  424. {
  425. tag: 'span',
  426. attributes: {
  427. class: 'child'
  428. }
  429. }
  430. ]
  431. } ).apply( el );
  432. expect( el.outerHTML ).to.be.equal( '<div class="parent">Children: <span class="child"></span></div>' );
  433. } );
  434. it( 'should work for deep DOM structure', () => {
  435. const childA = document.createElement( 'a' );
  436. const childB = document.createElement( 'b' );
  437. childA.textContent = 'anchor';
  438. childB.textContent = 'bold';
  439. el.appendChild( childA );
  440. el.appendChild( childB );
  441. expect( el.outerHTML ).to.equal( '<div><a>anchor</a><b>bold</b></div>' );
  442. const spy1 = testUtils.sinon.spy();
  443. const spy2 = testUtils.sinon.spy();
  444. const spy3 = testUtils.sinon.spy();
  445. observable.on( 'ku', spy1 );
  446. observable.on( 'kd', spy2 );
  447. observable.on( 'mo', spy3 );
  448. new Template( {
  449. tag: 'div',
  450. children: [
  451. {
  452. tag: 'a',
  453. on: {
  454. keyup: bind.to( 'ku' )
  455. },
  456. attributes: {
  457. class: bind.to( 'foo', val => 'applied-A-' + val ),
  458. id: 'applied-A'
  459. },
  460. children: [ 'Text applied to childA.' ]
  461. },
  462. {
  463. tag: 'b',
  464. on: {
  465. keydown: bind.to( 'kd' )
  466. },
  467. attributes: {
  468. class: bind.to( 'baz', val => 'applied-B-' + val ),
  469. id: 'applied-B'
  470. },
  471. children: [ 'Text applied to childB.' ]
  472. },
  473. 'Text which is not to be applied because it does NOT exist in original element.'
  474. ],
  475. on: {
  476. 'mouseover@a': bind.to( 'mo' )
  477. },
  478. attributes: {
  479. id: bind.to( 'foo', val => val.toUpperCase() ),
  480. class: bind.to( 'baz', val => 'applied-parent-' + val )
  481. }
  482. } ).apply( el );
  483. expect( el.outerHTML ).to.equal( '<div id="BAR" class="applied-parent-qux">' +
  484. '<a class="applied-A-bar" id="applied-A">Text applied to childA.</a>' +
  485. '<b class="applied-B-qux" id="applied-B">Text applied to childB.</b>' +
  486. '</div>' );
  487. observable.foo = 'updated';
  488. expect( el.outerHTML ).to.equal( '<div id="UPDATED" class="applied-parent-qux">' +
  489. '<a class="applied-A-updated" id="applied-A">Text applied to childA.</a>' +
  490. '<b class="applied-B-qux" id="applied-B">Text applied to childB.</b>' +
  491. '</div>' );
  492. document.body.appendChild( el );
  493. // Test "mouseover@a".
  494. dispatchEvent( el, 'mouseover' );
  495. dispatchEvent( childA, 'mouseover' );
  496. // Test "keyup".
  497. dispatchEvent( childA, 'keyup' );
  498. // Test "keydown".
  499. dispatchEvent( childB, 'keydown' );
  500. sinon.assert.calledOnce( spy1 );
  501. sinon.assert.calledOnce( spy2 );
  502. sinon.assert.calledOnce( spy3 );
  503. } );
  504. } );
  505. describe( 'bind', () => {
  506. it( 'returns object', () => {
  507. expect( Template.bind() ).to.be.an( 'object' );
  508. } );
  509. it( 'provides "to" and "if" interface', () => {
  510. const bind = Template.bind();
  511. expect( bind ).to.have.keys( 'to', 'if' );
  512. expect( bind.to ).to.be.a( 'function' );
  513. expect( bind.if ).to.be.a( 'function' );
  514. } );
  515. describe( 'event', () => {
  516. let observable, domEmitter, bind;
  517. beforeEach( () => {
  518. observable = new Model( {
  519. foo: 'bar',
  520. baz: 'qux'
  521. } );
  522. domEmitter = Object.create( DOMEmitterMixin );
  523. bind = Template.bind( observable, domEmitter );
  524. } );
  525. it( 'accepts plain binding', () => {
  526. const spy = testUtils.sinon.spy();
  527. setElement( {
  528. tag: 'p',
  529. on: {
  530. x: bind.to( 'a' ),
  531. }
  532. } );
  533. observable.on( 'a', spy );
  534. dispatchEvent( el, 'x' );
  535. sinon.assert.calledWithExactly( spy,
  536. sinon.match.has( 'name', 'a' ),
  537. sinon.match.has( 'target', el )
  538. );
  539. } );
  540. it( 'accepts an array of event bindings', () => {
  541. const spy1 = testUtils.sinon.spy();
  542. const spy2 = testUtils.sinon.spy();
  543. setElement( {
  544. tag: 'p',
  545. on: {
  546. x: [
  547. bind.to( 'a' ),
  548. bind.to( 'b' )
  549. ]
  550. }
  551. } );
  552. observable.on( 'a', spy1 );
  553. observable.on( 'b', spy2 );
  554. dispatchEvent( el, 'x' );
  555. sinon.assert.calledWithExactly( spy1,
  556. sinon.match.has( 'name', 'a' ),
  557. sinon.match.has( 'target', el )
  558. );
  559. sinon.assert.calledWithExactly( spy2,
  560. sinon.match.has( 'name', 'b' ),
  561. sinon.match.has( 'target', el )
  562. );
  563. } );
  564. it( 'accepts DOM selectors', () => {
  565. const spy1 = testUtils.sinon.spy();
  566. const spy2 = testUtils.sinon.spy();
  567. const spy3 = testUtils.sinon.spy();
  568. setElement( {
  569. tag: 'p',
  570. children: [
  571. {
  572. tag: 'span',
  573. attributes: {
  574. 'class': 'y',
  575. },
  576. on: {
  577. 'test@p': bind.to( 'c' )
  578. }
  579. },
  580. {
  581. tag: 'div',
  582. children: [
  583. {
  584. tag: 'span',
  585. attributes: {
  586. 'class': 'y',
  587. }
  588. }
  589. ],
  590. }
  591. ],
  592. on: {
  593. 'test@.y': bind.to( 'a' ),
  594. 'test@div': bind.to( 'b' )
  595. }
  596. } );
  597. observable.on( 'a', spy1 );
  598. observable.on( 'b', spy2 );
  599. observable.on( 'c', spy3 );
  600. // Test "test@p".
  601. dispatchEvent( el, 'test' );
  602. sinon.assert.callCount( spy1, 0 );
  603. sinon.assert.callCount( spy2, 0 );
  604. sinon.assert.callCount( spy3, 0 );
  605. // Test "test@.y".
  606. dispatchEvent( el.firstChild, 'test' );
  607. expect( spy1.firstCall.calledWithExactly(
  608. sinon.match.has( 'name', 'a' ),
  609. sinon.match.has( 'target', el.firstChild )
  610. ) ).to.be.true;
  611. sinon.assert.callCount( spy2, 0 );
  612. sinon.assert.callCount( spy3, 0 );
  613. // Test "test@div".
  614. dispatchEvent( el.lastChild, 'test' );
  615. sinon.assert.callCount( spy1, 1 );
  616. expect( spy2.firstCall.calledWithExactly(
  617. sinon.match.has( 'name', 'b' ),
  618. sinon.match.has( 'target', el.lastChild )
  619. ) ).to.be.true;
  620. sinon.assert.callCount( spy3, 0 );
  621. // Test "test@.y".
  622. dispatchEvent( el.lastChild.firstChild, 'test' );
  623. expect( spy1.secondCall.calledWithExactly(
  624. sinon.match.has( 'name', 'a' ),
  625. sinon.match.has( 'target', el.lastChild.firstChild )
  626. ) ).to.be.true;
  627. sinon.assert.callCount( spy2, 1 );
  628. sinon.assert.callCount( spy3, 0 );
  629. } );
  630. it( 'accepts function callbacks', () => {
  631. const spy1 = testUtils.sinon.spy();
  632. const spy2 = testUtils.sinon.spy();
  633. setElement( {
  634. tag: 'p',
  635. children: [
  636. {
  637. tag: 'span'
  638. }
  639. ],
  640. on: {
  641. x: bind.to( spy1 ),
  642. 'y@span': [
  643. bind.to( spy2 ),
  644. bind.to( 'c' )
  645. ]
  646. }
  647. } );
  648. dispatchEvent( el, 'x' );
  649. dispatchEvent( el.firstChild, 'y' );
  650. sinon.assert.calledWithExactly( spy1,
  651. sinon.match.has( 'target', el )
  652. );
  653. sinon.assert.calledWithExactly( spy2,
  654. sinon.match.has( 'target', el.firstChild )
  655. );
  656. } );
  657. it( 'supports event delegation', () => {
  658. const spy = testUtils.sinon.spy();
  659. setElement( {
  660. tag: 'p',
  661. children: [
  662. {
  663. tag: 'span'
  664. }
  665. ],
  666. on: {
  667. x: bind.to( 'a' ),
  668. }
  669. } );
  670. observable.on( 'a', spy );
  671. dispatchEvent( el.firstChild, 'x' );
  672. sinon.assert.calledWithExactly( spy,
  673. sinon.match.has( 'name', 'a' ),
  674. sinon.match.has( 'target', el.firstChild )
  675. );
  676. } );
  677. it( 'works for future elements', () => {
  678. const spy = testUtils.sinon.spy();
  679. setElement( {
  680. tag: 'p',
  681. on: {
  682. 'test@div': bind.to( 'a' )
  683. }
  684. } );
  685. observable.on( 'a', spy );
  686. const div = document.createElement( 'div' );
  687. el.appendChild( div );
  688. dispatchEvent( div, 'test' );
  689. sinon.assert.calledWithExactly( spy, sinon.match.has( 'name', 'a' ), sinon.match.has( 'target', div ) );
  690. } );
  691. } );
  692. describe( 'model', () => {
  693. let observable, emitter, bind;
  694. beforeEach( () => {
  695. observable = new Model( {
  696. foo: 'bar',
  697. baz: 'qux'
  698. } );
  699. emitter = Object.create( EmitterMixin );
  700. bind = Template.bind( observable, emitter );
  701. } );
  702. describe( 'to', () => {
  703. it( 'returns an object which describes the binding', () => {
  704. const spy = testUtils.sinon.spy();
  705. const binding = bind.to( 'foo', spy );
  706. expect( spy.called ).to.be.false;
  707. expect( binding ).to.have.keys( [ 'type', 'observable', 'eventNameOrFunction', 'emitter', 'attribute', 'callback' ] );
  708. expect( binding.observable ).to.equal( observable );
  709. expect( binding.callback ).to.equal( spy );
  710. expect( binding.attribute ).to.equal( 'foo' );
  711. } );
  712. it( 'allows binding attribute to the observable – simple (HTMLElement attribute)', () => {
  713. setElement( {
  714. tag: 'p',
  715. attributes: {
  716. 'class': bind.to( 'foo' )
  717. },
  718. children: [ 'abc' ]
  719. } );
  720. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  721. observable.foo = 'baz';
  722. expect( el.outerHTML ).to.equal( '<p class="baz">abc</p>' );
  723. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  724. } );
  725. it( 'allows binding attribute to the observable – simple (Text Node)', () => {
  726. setElement( {
  727. tag: 'p',
  728. children: [
  729. {
  730. text: bind.to( 'foo' )
  731. }
  732. ]
  733. } );
  734. expect( el.outerHTML ).to.equal( '<p>bar</p>' );
  735. observable.foo = 'baz';
  736. expect( el.outerHTML ).to.equal( '<p>baz</p>' );
  737. } );
  738. it( 'allows binding attribute to the observable – value processing', () => {
  739. const callback = value => value > 0 ? 'positive' : 'negative';
  740. setElement( {
  741. tag: 'p',
  742. attributes: {
  743. 'class': bind.to( 'foo', callback )
  744. },
  745. children: [
  746. {
  747. text: bind.to( 'foo', callback )
  748. }
  749. ]
  750. } );
  751. observable.foo = 3;
  752. expect( el.outerHTML ).to.equal( '<p class="positive">positive</p>' );
  753. observable.foo = -7;
  754. expect( el.outerHTML ).to.equal( '<p class="negative">negative</p>' );
  755. } );
  756. it( 'allows binding attribute to the observable – value processing (use Node)', () => {
  757. const callback = ( value, node ) => {
  758. return ( !!node.tagName && value > 0 ) ? 'HTMLElement positive' : '';
  759. };
  760. setElement( {
  761. tag: 'p',
  762. attributes: {
  763. 'class': bind.to( 'foo', callback )
  764. },
  765. children: [
  766. {
  767. text: bind.to( 'foo', callback )
  768. }
  769. ]
  770. } );
  771. observable.foo = 3;
  772. expect( el.outerHTML ).to.equal( '<p class="HTMLElement positive"></p>' );
  773. observable.foo = -7;
  774. expect( el.outerHTML ).to.equal( '<p></p>' );
  775. } );
  776. it( 'allows binding attribute to the observable – custom callback', () => {
  777. setElement( {
  778. tag: 'p',
  779. attributes: {
  780. 'class': bind.to( 'foo', ( value, el ) => {
  781. el.innerHTML = value;
  782. if ( value == 'changed' ) {
  783. return value;
  784. }
  785. } )
  786. }
  787. } );
  788. observable.foo = 'moo';
  789. expect( el.outerHTML ).to.equal( '<p class="undefined">moo</p>' );
  790. observable.foo = 'changed';
  791. expect( el.outerHTML ).to.equal( '<p class="changed">changed</p>' );
  792. } );
  793. it( 'allows binding attribute to the observable – array of bindings (HTMLElement attribute)', () => {
  794. setElement( {
  795. tag: 'p',
  796. attributes: {
  797. 'class': [
  798. 'ck-class',
  799. bind.to( 'foo' ),
  800. bind.to( 'baz' ),
  801. bind.to( 'foo', value => `foo-is-${value}` ),
  802. 'ck-end'
  803. ]
  804. },
  805. children: [ 'abc' ]
  806. } );
  807. observable.foo = 'a';
  808. observable.baz = 'b';
  809. expect( el.outerHTML ).to.equal( '<p class="ck-class a b foo-is-a ck-end">abc</p>' );
  810. observable.foo = 'c';
  811. observable.baz = 'd';
  812. expect( el.outerHTML ).to.equal( '<p class="ck-class c d foo-is-c ck-end">abc</p>' );
  813. } );
  814. it( 'allows binding attribute to the observable – array of bindings (Text Node)', () => {
  815. setElement( {
  816. tag: 'p',
  817. attributes: {
  818. },
  819. children: [
  820. {
  821. text: [
  822. 'ck-class',
  823. bind.to( 'foo' ),
  824. bind.to( 'baz' ),
  825. bind.to( 'foo', value => `foo-is-${value}` ),
  826. 'ck-end'
  827. ]
  828. }
  829. ]
  830. } );
  831. observable.foo = 'a';
  832. observable.baz = 'b';
  833. expect( el.outerHTML ).to.equal( '<p>ck-class a b foo-is-a ck-end</p>' );
  834. observable.foo = 'c';
  835. observable.baz = 'd';
  836. expect( el.outerHTML ).to.equal( '<p>ck-class c d foo-is-c ck-end</p>' );
  837. } );
  838. it( 'allows binding attribute to the observable – falsy values', () => {
  839. setElement( {
  840. tag: 'p',
  841. attributes: {
  842. 'class': bind.to( 'foo' )
  843. },
  844. children: [ 'abc' ]
  845. } );
  846. observable.foo = 'bar';
  847. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  848. observable.foo = false;
  849. expect( el.outerHTML ).to.equal( '<p class="false">abc</p>' );
  850. observable.foo = null;
  851. expect( el.outerHTML ).to.equal( '<p class="null">abc</p>' );
  852. observable.foo = undefined;
  853. expect( el.outerHTML ).to.equal( '<p class="undefined">abc</p>' );
  854. observable.foo = 0;
  855. expect( el.outerHTML ).to.equal( '<p class="0">abc</p>' );
  856. observable.foo = '';
  857. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  858. } );
  859. it( 'allows binding attribute to the observable – a custom namespace', () => {
  860. setElement( {
  861. tag: 'p',
  862. attributes: {
  863. class: {
  864. ns: 'foo',
  865. value: bind.to( 'foo' )
  866. },
  867. custom: {
  868. ns: 'foo',
  869. value: [
  870. bind.to( 'foo' ),
  871. bind.to( 'baz' )
  872. ]
  873. }
  874. },
  875. children: [ 'abc' ]
  876. } );
  877. observable.foo = 'bar';
  878. expect( el.outerHTML ).to.equal( '<p class="bar" custom="bar qux">abc</p>' );
  879. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  880. observable.foo = 'baz';
  881. expect( el.outerHTML ).to.equal( '<p class="baz" custom="baz qux">abc</p>' );
  882. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  883. } );
  884. } );
  885. describe( 'if', () => {
  886. it( 'returns an object which describes the binding', () => {
  887. const spy = testUtils.sinon.spy();
  888. const binding = bind.if( 'foo', 'whenTrue', spy );
  889. expect( spy.called ).to.be.false;
  890. expect( binding ).to.have.keys( [ 'type', 'observable', 'emitter', 'attribute', 'callback', 'valueIfTrue' ] );
  891. expect( binding.observable ).to.equal( observable );
  892. expect( binding.callback ).to.equal( spy );
  893. expect( binding.attribute ).to.equal( 'foo' );
  894. expect( binding.valueIfTrue ).to.equal( 'whenTrue' );
  895. } );
  896. it( 'allows binding attribute to the observable – presence of an attribute (HTMLElement attribute)', () => {
  897. setElement( {
  898. tag: 'p',
  899. attributes: {
  900. 'class': bind.if( 'foo' )
  901. },
  902. children: [ 'abc' ]
  903. } );
  904. observable.foo = true;
  905. expect( el.outerHTML ).to.equal( '<p class="">abc</p>' );
  906. observable.foo = false;
  907. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  908. observable.foo = 'bar';
  909. expect( el.outerHTML ).to.equal( '<p class="">abc</p>' );
  910. } );
  911. // TODO: Is this alright? It makes sense but it's pretty useless. Text Node cannot be
  912. // removed just like an attribute of some HTMLElement.
  913. it( 'allows binding attribute to the observable – presence of an attribute (Text Node)', () => {
  914. setElement( {
  915. tag: 'p',
  916. children: [
  917. {
  918. text: bind.if( 'foo' )
  919. }
  920. ]
  921. } );
  922. observable.foo = true;
  923. expect( el.outerHTML ).to.equal( '<p></p>' );
  924. observable.foo = false;
  925. expect( el.outerHTML ).to.equal( '<p></p>' );
  926. observable.foo = 'bar';
  927. expect( el.outerHTML ).to.equal( '<p></p>' );
  928. } );
  929. it( 'allows binding attribute to the observable – value of an attribute (HTMLElement attribute)', () => {
  930. setElement( {
  931. tag: 'p',
  932. attributes: {
  933. 'class': bind.if( 'foo', 'bar' )
  934. },
  935. children: [ 'abc' ]
  936. } );
  937. observable.foo = 'bar';
  938. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  939. observable.foo = false;
  940. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  941. observable.foo = 64;
  942. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  943. } );
  944. it( 'allows binding attribute to the observable – value of an attribute (Text Node)', () => {
  945. setElement( {
  946. tag: 'p',
  947. children: [
  948. {
  949. text: bind.if( 'foo', 'bar' )
  950. }
  951. ]
  952. } );
  953. observable.foo = 'bar';
  954. expect( el.outerHTML ).to.equal( '<p>bar</p>' );
  955. observable.foo = false;
  956. expect( el.outerHTML ).to.equal( '<p></p>' );
  957. observable.foo = 64;
  958. expect( el.outerHTML ).to.equal( '<p>bar</p>' );
  959. } );
  960. it( 'allows binding attribute to the observable – array of bindings (HTMLElement attribute)', () => {
  961. setElement( {
  962. tag: 'p',
  963. attributes: {
  964. 'class': [
  965. 'ck-class',
  966. bind.if( 'foo', 'foo-set' ),
  967. bind.if( 'bar', 'bar-not-set', ( value ) => !value ),
  968. 'ck-end'
  969. ]
  970. },
  971. children: [ 'abc' ]
  972. } );
  973. observable.foo = observable.bar = true;
  974. expect( el.outerHTML ).to.equal( '<p class="ck-class foo-set ck-end">abc</p>' );
  975. observable.foo = observable.bar = false;
  976. expect( el.outerHTML ).to.equal( '<p class="ck-class bar-not-set ck-end">abc</p>' );
  977. } );
  978. it( 'allows binding attribute to the observable – value of an attribute processed by a callback', () => {
  979. setElement( {
  980. tag: 'p',
  981. attributes: {
  982. 'class': bind.if( 'foo', 'there–is–no–foo', value => !value )
  983. },
  984. children: [ 'abc' ]
  985. } );
  986. observable.foo = 'bar';
  987. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  988. observable.foo = false;
  989. expect( el.outerHTML ).to.equal( '<p class="there–is–no–foo">abc</p>' );
  990. observable.foo = 64;
  991. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  992. } );
  993. it( 'allows binding attribute to the observable – value of an attribute processed by a callback (use Node)', () => {
  994. setElement( {
  995. tag: 'p',
  996. attributes: {
  997. 'class': bind.if( 'foo', 'eqls-tag-name', ( value, el ) => el.tagName === value )
  998. },
  999. children: [ 'abc' ]
  1000. } );
  1001. observable.foo = 'bar';
  1002. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1003. observable.foo = 'P';
  1004. expect( el.outerHTML ).to.equal( '<p class="eqls-tag-name">abc</p>' );
  1005. observable.foo = 64;
  1006. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1007. } );
  1008. it( 'allows binding attribute to the observable – falsy values', () => {
  1009. setElement( {
  1010. tag: 'p',
  1011. attributes: {
  1012. 'class': bind.if( 'foo', 'foo-is-set' )
  1013. },
  1014. children: [ 'abc' ]
  1015. } );
  1016. observable.foo = 'bar';
  1017. expect( el.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
  1018. observable.foo = false;
  1019. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1020. observable.foo = null;
  1021. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1022. observable.foo = undefined;
  1023. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1024. observable.foo = '';
  1025. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1026. observable.foo = 0;
  1027. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1028. } );
  1029. } );
  1030. it( 'works with Template#apply() – root element', () => {
  1031. new Template( {
  1032. tag: 'div',
  1033. attributes: {
  1034. class: bind.to( 'foo' )
  1035. }
  1036. } ).apply( el );
  1037. expect( el.getAttribute( 'class' ) ).to.equal( 'bar' );
  1038. observable.foo = 'baz';
  1039. expect( el.getAttribute( 'class' ) ).to.equal( 'baz' );
  1040. } );
  1041. it( 'works with Template#apply() – children', () => {
  1042. const el = document.createElement( 'div' );
  1043. const child = document.createElement( 'span' );
  1044. child.textContent = 'foo';
  1045. el.appendChild( child );
  1046. new Template( {
  1047. tag: 'div',
  1048. children: [
  1049. {
  1050. tag: 'span',
  1051. children: [
  1052. {
  1053. text: bind.to( 'foo' )
  1054. }
  1055. ]
  1056. }
  1057. ]
  1058. } ).apply( el );
  1059. expect( child.textContent ).to.equal( 'bar' );
  1060. observable.foo = 'baz';
  1061. expect( child.textContent ).to.equal( 'baz' );
  1062. } );
  1063. } );
  1064. } );
  1065. describe( 'extend', () => {
  1066. let observable, emitter, bind;
  1067. beforeEach( () => {
  1068. observable = new Model( {
  1069. foo: 'bar',
  1070. baz: 'qux'
  1071. } );
  1072. emitter = Object.create( DOMEmitterMixin );
  1073. bind = Template.bind( observable, emitter );
  1074. } );
  1075. it( 'does not modify passed definition', () => {
  1076. const def = {
  1077. tag: 'p',
  1078. attributes: {
  1079. a: 'foo',
  1080. }
  1081. };
  1082. const ext = {
  1083. attributes: {
  1084. b: 'bar'
  1085. }
  1086. };
  1087. const tpl = new Template( def );
  1088. Template.extend( tpl, ext );
  1089. expect( def.attributes.a ).to.equal( 'foo' );
  1090. expect( ext.attributes.b ).to.equal( 'bar' );
  1091. expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
  1092. expect( tpl.definition.attributes.b[ 0 ] ).to.equal( 'bar' );
  1093. } );
  1094. describe( 'attributes', () => {
  1095. it( 'extends existing - simple', () => {
  1096. extensionTest(
  1097. {
  1098. tag: 'p',
  1099. attributes: {
  1100. a: 'b'
  1101. }
  1102. },
  1103. {
  1104. attributes: {
  1105. a: 'c'
  1106. }
  1107. },
  1108. '<p a="b c"></p>'
  1109. );
  1110. } );
  1111. it( 'extends existing - complex #1', () => {
  1112. extensionTest(
  1113. {
  1114. tag: 'p',
  1115. attributes: {
  1116. a: [ 'b', 'c' ]
  1117. }
  1118. },
  1119. {
  1120. attributes: {
  1121. a: 'd'
  1122. }
  1123. },
  1124. '<p a="b c d"></p>'
  1125. );
  1126. } );
  1127. it( 'extends existing - complex #2', () => {
  1128. extensionTest(
  1129. {
  1130. tag: 'p',
  1131. attributes: {
  1132. a: {
  1133. value: 'b'
  1134. }
  1135. }
  1136. },
  1137. {
  1138. attributes: {
  1139. a: [ 'c', 'd' ]
  1140. }
  1141. },
  1142. '<p a="b c d"></p>'
  1143. );
  1144. } );
  1145. it( 'extends existing - complex #3', () => {
  1146. extensionTest(
  1147. {
  1148. tag: 'p',
  1149. attributes: {
  1150. a: [ 'b' ]
  1151. }
  1152. },
  1153. {
  1154. attributes: {
  1155. a: [ 'c', 'd' ]
  1156. }
  1157. },
  1158. '<p a="b c d"></p>'
  1159. );
  1160. } );
  1161. it( 'extends existing - bindings #1', () => {
  1162. const el = extensionTest(
  1163. {
  1164. tag: 'p',
  1165. attributes: {
  1166. a: bind.to( 'foo' )
  1167. }
  1168. },
  1169. {
  1170. attributes: {
  1171. a: [ 'c', 'd' ]
  1172. }
  1173. },
  1174. '<p a="bar c d"></p>'
  1175. );
  1176. observable.foo = 'baz';
  1177. expect( el.outerHTML ).to.equal( '<p a="baz c d"></p>' );
  1178. } );
  1179. it( 'extends existing - bindings #2', () => {
  1180. const el = extensionTest(
  1181. {
  1182. tag: 'p',
  1183. attributes: {
  1184. a: [ 'b', bind.to( 'foo' ) ]
  1185. }
  1186. },
  1187. {
  1188. attributes: {
  1189. a: [ 'c', bind.to( 'baz' ) ]
  1190. }
  1191. },
  1192. '<p a="b bar c qux"></p>'
  1193. );
  1194. observable.foo = 'abc';
  1195. observable.baz = 'def';
  1196. expect( el.outerHTML ).to.equal( '<p a="b abc c def"></p>' );
  1197. } );
  1198. it( 'creates new - no attributes', () => {
  1199. extensionTest(
  1200. {
  1201. tag: 'p',
  1202. },
  1203. {
  1204. attributes: {
  1205. c: 'd'
  1206. }
  1207. },
  1208. '<p c="d"></p>'
  1209. );
  1210. } );
  1211. it( 'creates new - simple', () => {
  1212. extensionTest(
  1213. {
  1214. tag: 'p',
  1215. attributes: {
  1216. a: 'b'
  1217. }
  1218. },
  1219. {
  1220. attributes: {
  1221. c: 'd'
  1222. }
  1223. },
  1224. '<p a="b" c="d"></p>'
  1225. );
  1226. } );
  1227. it( 'creates new - array', () => {
  1228. extensionTest(
  1229. {
  1230. tag: 'p',
  1231. attributes: {
  1232. a: 'b'
  1233. }
  1234. },
  1235. {
  1236. attributes: {
  1237. c: [ 'd', 'e' ]
  1238. }
  1239. },
  1240. '<p a="b" c="d e"></p>'
  1241. );
  1242. } );
  1243. it( 'creates new - bindings #1', () => {
  1244. const el = extensionTest(
  1245. {
  1246. tag: 'p',
  1247. attributes: {
  1248. a: 'b'
  1249. }
  1250. },
  1251. {
  1252. attributes: {
  1253. c: bind.to( 'foo' )
  1254. }
  1255. },
  1256. '<p a="b" c="bar"></p>'
  1257. );
  1258. observable.foo = 'abc';
  1259. expect( el.outerHTML ).to.equal( '<p a="b" c="abc"></p>' );
  1260. } );
  1261. it( 'creates new - bindings #2', () => {
  1262. const el = extensionTest(
  1263. {
  1264. tag: 'p',
  1265. attributes: {
  1266. a: 'b'
  1267. }
  1268. },
  1269. {
  1270. attributes: {
  1271. c: [ 'd', bind.to( 'foo' ) ]
  1272. }
  1273. },
  1274. '<p a="b" c="d bar"></p>'
  1275. );
  1276. observable.foo = 'abc';
  1277. expect( el.outerHTML ).to.equal( '<p a="b" c="d abc"></p>' );
  1278. } );
  1279. } );
  1280. describe( 'text', () => {
  1281. it( 'extends existing - simple', () => {
  1282. const el = extensionTest(
  1283. {
  1284. tag: 'p',
  1285. children: [
  1286. 'foo'
  1287. ]
  1288. },
  1289. {
  1290. children: [
  1291. 'bar'
  1292. ]
  1293. },
  1294. '<p>foobar</p>'
  1295. );
  1296. expect( el.childNodes ).to.have.length( 1 );
  1297. } );
  1298. it( 'extends existing - complex #1', () => {
  1299. const el = extensionTest(
  1300. {
  1301. tag: 'p',
  1302. children: [
  1303. { text: 'foo' }
  1304. ]
  1305. },
  1306. {
  1307. children: [
  1308. 'bar'
  1309. ]
  1310. },
  1311. '<p>foobar</p>'
  1312. );
  1313. expect( el.childNodes ).to.have.length( 1 );
  1314. } );
  1315. it( 'extends existing - complex #2', () => {
  1316. const el = extensionTest(
  1317. {
  1318. tag: 'p',
  1319. children: [
  1320. { text: 'foo' }
  1321. ]
  1322. },
  1323. {
  1324. children: [
  1325. { text: 'bar' }
  1326. ]
  1327. },
  1328. '<p>foobar</p>'
  1329. );
  1330. expect( el.childNodes ).to.have.length( 1 );
  1331. } );
  1332. it( 'extends existing - bindings #1', () => {
  1333. const el = extensionTest(
  1334. {
  1335. tag: 'p',
  1336. children: [
  1337. { text: bind.to( 'foo' ) }
  1338. ]
  1339. },
  1340. {
  1341. children: [
  1342. 'abc'
  1343. ]
  1344. },
  1345. '<p>bar abc</p>'
  1346. );
  1347. observable.foo = 'asd';
  1348. expect( el.outerHTML ).to.equal( '<p>asd abc</p>' );
  1349. expect( el.childNodes ).to.have.length( 1 );
  1350. } );
  1351. it( 'extends existing - bindings #2', () => {
  1352. const el = extensionTest(
  1353. {
  1354. tag: 'p',
  1355. children: [
  1356. 'abc'
  1357. ]
  1358. },
  1359. {
  1360. children: [
  1361. { text: bind.to( 'foo' ) }
  1362. ]
  1363. },
  1364. '<p>abc bar</p>'
  1365. );
  1366. observable.foo = 'asd';
  1367. expect( el.outerHTML ).to.equal( '<p>abc asd</p>' );
  1368. expect( el.childNodes ).to.have.length( 1 );
  1369. } );
  1370. it( 'extends existing - bindings #3', () => {
  1371. const el = extensionTest(
  1372. {
  1373. tag: 'p',
  1374. children: [
  1375. { text: bind.to( 'foo' ) },
  1376. 'X'
  1377. ]
  1378. },
  1379. {
  1380. children: [
  1381. { text: bind.to( 'baz' ) },
  1382. 'Y'
  1383. ]
  1384. },
  1385. '<p>bar quxXY</p>'
  1386. );
  1387. observable.foo = 'A';
  1388. observable.baz = 'B';
  1389. expect( el.outerHTML ).to.equal( '<p>A BXY</p>' );
  1390. expect( el.childNodes ).to.have.length( 2 );
  1391. } );
  1392. } );
  1393. describe( 'children', () => {
  1394. it( 'should throw when the number of children does not correspond', () => {
  1395. expect( () => {
  1396. extensionTest(
  1397. {
  1398. tag: 'p',
  1399. children: [
  1400. 'foo'
  1401. ]
  1402. },
  1403. {
  1404. children: [
  1405. 'foo',
  1406. 'bar'
  1407. ]
  1408. },
  1409. 'it should fail'
  1410. );
  1411. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1412. } );
  1413. it( 'should throw when no children in target but extending one', () => {
  1414. expect( () => {
  1415. extensionTest(
  1416. {
  1417. tag: 'p',
  1418. },
  1419. {
  1420. children: [
  1421. {
  1422. tag: 'b'
  1423. }
  1424. ]
  1425. },
  1426. 'it should fail'
  1427. );
  1428. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1429. } );
  1430. it( 'should throw when the number of children does not correspond on some deeper level', () => {
  1431. expect( () => {
  1432. extensionTest(
  1433. {
  1434. tag: 'p',
  1435. children: [
  1436. {
  1437. tag: 'span',
  1438. attributes: {
  1439. class: 'A'
  1440. },
  1441. children: [
  1442. 'A',
  1443. {
  1444. tag: 'span',
  1445. attributes: {
  1446. class: 'AA'
  1447. },
  1448. children: [
  1449. 'AA'
  1450. ]
  1451. }
  1452. ]
  1453. }
  1454. ]
  1455. },
  1456. {
  1457. children: [
  1458. {
  1459. attributes: {
  1460. class: 'B'
  1461. },
  1462. children: [
  1463. 'B'
  1464. ]
  1465. }
  1466. ]
  1467. },
  1468. 'it should fail'
  1469. );
  1470. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1471. } );
  1472. it( 'extends existing - simple', () => {
  1473. extensionTest(
  1474. {
  1475. tag: 'p',
  1476. children: [
  1477. {
  1478. tag: 'span',
  1479. attributes: {
  1480. class: 'foo'
  1481. }
  1482. }
  1483. ]
  1484. },
  1485. {
  1486. children: [
  1487. {
  1488. tag: 'span',
  1489. attributes: {
  1490. class: 'bar'
  1491. }
  1492. }
  1493. ]
  1494. },
  1495. '<p><span class="foo bar"></span></p>'
  1496. );
  1497. } );
  1498. it( 'extends existing - complex', () => {
  1499. extensionTest(
  1500. {
  1501. tag: 'p',
  1502. children: [
  1503. {
  1504. tag: 'span',
  1505. attributes: {
  1506. class: 'A'
  1507. },
  1508. children: [
  1509. 'A',
  1510. {
  1511. tag: 'span',
  1512. attributes: {
  1513. class: 'AA'
  1514. },
  1515. children: [
  1516. 'AA'
  1517. ]
  1518. }
  1519. ]
  1520. }
  1521. ]
  1522. },
  1523. {
  1524. children: [
  1525. {
  1526. tag: 'span',
  1527. attributes: {
  1528. class: 'B'
  1529. },
  1530. children: [
  1531. 'B',
  1532. {
  1533. tag: 'span',
  1534. attributes: {
  1535. class: 'BB'
  1536. },
  1537. children: [
  1538. 'BB'
  1539. ]
  1540. }
  1541. ]
  1542. }
  1543. ]
  1544. },
  1545. '<p><span class="A B">AB<span class="AA BB">AABB</span></span></p>'
  1546. );
  1547. } );
  1548. it( 'allows extending a particular child', () => {
  1549. const template = new Template( {
  1550. tag: 'p',
  1551. children: [
  1552. {
  1553. tag: 'span',
  1554. attributes: {
  1555. class: 'foo'
  1556. }
  1557. }
  1558. ]
  1559. } );
  1560. Template.extend( template.definition.children[ 0 ], {
  1561. attributes: {
  1562. class: 'bar'
  1563. }
  1564. } );
  1565. expect( template.render().outerHTML ).to.equal( '<p><span class="foo bar"></span></p>' );
  1566. } );
  1567. it( 'allows extending a particular child – recursively', () => {
  1568. const template = new Template( {
  1569. tag: 'p',
  1570. children: [
  1571. {
  1572. tag: 'span',
  1573. attributes: {
  1574. class: 'A'
  1575. },
  1576. children: [
  1577. 'A',
  1578. {
  1579. tag: 'span',
  1580. attributes: {
  1581. class: 'AA'
  1582. },
  1583. children: [
  1584. 'AA'
  1585. ]
  1586. }
  1587. ]
  1588. }
  1589. ]
  1590. } );
  1591. Template.extend( template.definition.children[ 0 ], {
  1592. attributes: {
  1593. class: 'B',
  1594. },
  1595. children: [
  1596. 'B',
  1597. {
  1598. attributes: {
  1599. class: 'BB'
  1600. }
  1601. }
  1602. ]
  1603. } );
  1604. expect( template.render().outerHTML ).to.equal( '<p><span class="A B">AB<span class="AA BB">AA</span></span></p>' );
  1605. } );
  1606. } );
  1607. describe( 'listeners', () => {
  1608. it( 'extends existing', () => {
  1609. const spy1 = testUtils.sinon.spy();
  1610. const spy2 = testUtils.sinon.spy();
  1611. const spy3 = testUtils.sinon.spy();
  1612. const spy4 = testUtils.sinon.spy();
  1613. const spy5 = testUtils.sinon.spy();
  1614. observable.on( 'A', spy1 );
  1615. observable.on( 'C', spy2 );
  1616. observable.on( 'B', spy3 );
  1617. observable.on( 'D', spy4 );
  1618. const el = extensionTest(
  1619. {
  1620. tag: 'p',
  1621. children: [
  1622. {
  1623. tag: 'span'
  1624. }
  1625. ],
  1626. on: {
  1627. click: bind.to( 'A' ),
  1628. 'click@span': [
  1629. bind.to( 'B' ),
  1630. bind.to( spy5 )
  1631. ]
  1632. }
  1633. },
  1634. {
  1635. on: {
  1636. click: bind.to( 'C' ),
  1637. 'click@span': bind.to( 'D' )
  1638. }
  1639. },
  1640. '<p><span></span></p>'
  1641. );
  1642. dispatchEvent( el, 'click' );
  1643. expect( spy1.calledOnce ).to.be.true;
  1644. expect( spy2.calledOnce ).to.be.true;
  1645. expect( spy3.called ).to.be.false;
  1646. expect( spy4.called ).to.be.false;
  1647. expect( spy5.called ).to.be.false;
  1648. dispatchEvent( el.firstChild, 'click' );
  1649. expect( spy1.calledTwice ).to.be.true;
  1650. expect( spy2.calledTwice ).to.be.true;
  1651. expect( spy3.calledOnce ).to.be.true;
  1652. expect( spy4.calledOnce ).to.be.true;
  1653. expect( spy5.calledOnce ).to.be.true;
  1654. } );
  1655. it( 'creates new', () => {
  1656. const spy1 = testUtils.sinon.spy();
  1657. const spy2 = testUtils.sinon.spy();
  1658. const spy3 = testUtils.sinon.spy();
  1659. observable.on( 'A', spy1 );
  1660. observable.on( 'B', spy2 );
  1661. const el = extensionTest(
  1662. {
  1663. tag: 'p',
  1664. children: [
  1665. {
  1666. tag: 'span'
  1667. }
  1668. ],
  1669. },
  1670. {
  1671. on: {
  1672. click: bind.to( 'A' ),
  1673. 'click@span': [
  1674. bind.to( 'B' ),
  1675. bind.to( spy3 )
  1676. ]
  1677. }
  1678. },
  1679. '<p><span></span></p>'
  1680. );
  1681. dispatchEvent( el, 'click' );
  1682. expect( spy1.calledOnce ).to.be.true;
  1683. expect( spy2.called ).to.be.false;
  1684. expect( spy3.called ).to.be.false;
  1685. dispatchEvent( el.firstChild, 'click' );
  1686. expect( spy1.calledTwice ).to.be.true;
  1687. expect( spy2.calledOnce ).to.be.true;
  1688. expect( spy3.calledOnce ).to.be.true;
  1689. } );
  1690. } );
  1691. } );
  1692. } );
  1693. function setElement( template ) {
  1694. el = new Template( template ).render();
  1695. document.body.appendChild( el );
  1696. }
  1697. function extensionTest( base, extension, expectedHtml ) {
  1698. const template = new Template( base );
  1699. Template.extend( template, extension );
  1700. const el = template.render();
  1701. document.body.appendChild( el );
  1702. expect( el.outerHTML ).to.equal( expectedHtml );
  1703. return el;
  1704. }
  1705. function dispatchEvent( el, domEvtName ) {
  1706. if ( !el.parentNode ) {
  1707. throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
  1708. }
  1709. el.dispatchEvent( new Event( domEvtName, {
  1710. bubbles: true
  1711. } ) );
  1712. }