template.js 45 KB

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