template.js 45 KB

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