8
0

template.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094
  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. /* globals HTMLElement, Event, document */
  7. import testUtils from '/tests/core/_utils/utils.js';
  8. import Template from '/ckeditor5/ui/template.js';
  9. import Model from '/ckeditor5/ui/model.js';
  10. import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
  11. import DOMEmitterMixin from '/ckeditor5/ui/domemittermixin.js';
  12. testUtils.createSinonSandbox();
  13. let el, text;
  14. describe( 'Template', () => {
  15. describe( 'constructor', () => {
  16. it( 'accepts template definition', () => {
  17. const def = {
  18. tag: 'p'
  19. };
  20. expect( new Template( def ).definition ).to.not.equal( def );
  21. expect( new Template( def ).definition.tag ).to.equal( 'p' );
  22. } );
  23. it( 'normalizes template definition', () => {
  24. const bind = Template.bind( new Model( {} ), Object.create( DOMEmitterMixin ) );
  25. const tpl = new Template( {
  26. tag: 'p',
  27. attributes: {
  28. a: 'foo',
  29. b: [ 'bar', 'baz' ],
  30. c: {
  31. ns: 'abc',
  32. value: bind.to( 'qux' )
  33. }
  34. },
  35. children: [
  36. {
  37. text: 'content'
  38. },
  39. {
  40. text: bind.to( 'x' )
  41. },
  42. 'abc',
  43. {
  44. text: [ 'a', 'b' ]
  45. }
  46. ],
  47. on: {
  48. 'a@span': bind.to( 'b' ),
  49. 'b@span': bind.to( () => {} ),
  50. 'c@span': [
  51. bind.to( 'c' ),
  52. bind.to( () => {} )
  53. ]
  54. }
  55. } );
  56. const def = tpl.definition;
  57. expect( def.attributes.a[ 0 ] ).to.equal( 'foo' );
  58. expect( def.attributes.b[ 0 ] ).to.equal( 'bar' );
  59. expect( def.attributes.b[ 1 ] ).to.equal( 'baz' );
  60. expect( def.attributes.c[ 0 ].value[ 0 ].type ).to.be.a( 'symbol' );
  61. expect( def.children[ 0 ].text[ 0 ] ).to.equal( 'content' );
  62. expect( def.children[ 1 ].text[ 0 ].type ).to.be.a( 'symbol' );
  63. expect( def.children[ 2 ].text[ 0 ] ).to.equal( 'abc' );
  64. expect( def.children[ 3 ].text[ 0 ] ).to.equal( 'a' );
  65. expect( def.children[ 3 ].text[ 1 ] ).to.equal( 'b' );
  66. expect( def.on[ 'a@span' ][ 0 ].type ).to.be.a( 'symbol' );
  67. expect( def.on[ 'b@span' ][ 0 ].type ).to.be.a( 'symbol' );
  68. expect( def.on[ 'c@span' ][ 0 ].type ).to.be.a( 'symbol' );
  69. expect( def.on[ 'c@span' ][ 1 ].type ).to.be.a( 'symbol' );
  70. } );
  71. it( 'does not modify passed definition', () => {
  72. const def = {
  73. tag: 'p',
  74. attributes: {
  75. a: 'foo',
  76. },
  77. children: [
  78. {
  79. tag: 'span'
  80. }
  81. ]
  82. };
  83. const tpl = new Template( def );
  84. expect( def ).to.not.equal( tpl.definition );
  85. expect( def.attributes ).to.not.equal( tpl.definition.attributes );
  86. expect( def.children ).to.not.equal( tpl.definition.children );
  87. expect( def.children[ 0 ] ).to.not.equal( tpl.definition.children[ 0 ] );
  88. expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
  89. expect( def.attributes.a ).to.equal( 'foo' );
  90. } );
  91. } );
  92. describe( 'render', () => {
  93. it( 'throws when wrong template definition', () => {
  94. expect( () => {
  95. new Template( {} ).render();
  96. } ).to.throwCKEditorError( /ui-template-wrong-syntax/ );
  97. expect( () => {
  98. new Template( {
  99. tag: 'p',
  100. text: 'foo'
  101. } ).render();
  102. } ).to.throwCKEditorError( /ui-template-wrong-syntax/ );
  103. } );
  104. it( 'creates HTMLElement', () => {
  105. const el = new Template( {
  106. tag: 'p',
  107. } ).render();
  108. expect( el ).to.be.instanceof( HTMLElement );
  109. expect( el.parentNode ).to.be.null;
  110. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  111. expect( el.namespaceURI ).to.be.equal( 'http://www.w3.org/1999/xhtml' );
  112. } );
  113. it( 'creates an element in a custom namespace', () => {
  114. const el = new Template( {
  115. tag: 'p',
  116. ns: 'foo'
  117. } ).render();
  118. expect( el.namespaceURI ).to.be.equal( 'foo' );
  119. } );
  120. it( 'renders HTMLElement attributes', () => {
  121. const el = new Template( {
  122. tag: 'p',
  123. attributes: {
  124. 'class': [ 'a', 'b' ],
  125. x: 'bar'
  126. },
  127. children: [ 'foo' ]
  128. } ).render();
  129. expect( el ).to.be.instanceof( HTMLElement );
  130. expect( el.parentNode ).to.be.null;
  131. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  132. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  133. } );
  134. it( 'renders HTMLElement attributes – empty', () => {
  135. const el = new Template( {
  136. tag: 'p',
  137. attributes: {
  138. class: '',
  139. x: [ '', '' ]
  140. },
  141. children: [ 'foo' ]
  142. } ).render();
  143. expect( el.outerHTML ).to.be.equal( '<p>foo</p>' );
  144. } );
  145. it( 'renders HTMLElement attributes – falsy values', () => {
  146. const el = new Template( {
  147. tag: 'p',
  148. attributes: {
  149. class: false,
  150. x: [ '', null, undefined, false ],
  151. y: [ 'foo', null, undefined, false ]
  152. },
  153. children: [ 'foo' ]
  154. } ).render();
  155. expect( el.outerHTML ).to.be.equal( '<p y="foo">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. color: undefined
  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.throwCKEditorError( /ui-template-wrong-syntax/ );
  391. } );
  392. it( 'throws when no HTMLElement passed', () => {
  393. expect( () => {
  394. new Template( {
  395. tag: 'p'
  396. } ).apply();
  397. } ).to.throwCKEditorError( /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>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. simple: bind.to( 'foo' ),
  876. complex: [ null, bind.to( 'foo' ), undefined, false ],
  877. zero: [ 0, bind.to( 'foo' ) ],
  878. emptystring: [ '', bind.to( 'foo' ) ]
  879. },
  880. children: [ 'abc' ]
  881. } );
  882. observable.foo = 'bar';
  883. expect( el.outerHTML ).to.equal( '<p simple="bar" complex="bar" zero="0 bar" emptystring="bar">abc</p>' );
  884. observable.foo = 0;
  885. expect( el.outerHTML ).to.equal( '<p simple="0" complex="0" zero="0 0" emptystring="0">abc</p>' );
  886. observable.foo = false;
  887. expect( el.outerHTML ).to.equal( '<p zero="0">abc</p>' );
  888. observable.foo = null;
  889. expect( el.outerHTML ).to.equal( '<p zero="0">abc</p>' );
  890. observable.foo = undefined;
  891. expect( el.outerHTML ).to.equal( '<p zero="0">abc</p>' );
  892. observable.foo = '';
  893. expect( el.outerHTML ).to.equal( '<p zero="0">abc</p>' );
  894. } );
  895. it( 'allows binding attribute to the observable – a custom namespace', () => {
  896. setElement( {
  897. tag: 'p',
  898. attributes: {
  899. class: {
  900. ns: 'foo',
  901. value: bind.to( 'foo' )
  902. },
  903. custom: {
  904. ns: 'foo',
  905. value: [
  906. bind.to( 'foo' ),
  907. bind.to( 'baz' )
  908. ]
  909. }
  910. },
  911. children: [ 'abc' ]
  912. } );
  913. observable.foo = 'bar';
  914. expect( el.outerHTML ).to.equal( '<p class="bar" custom="bar qux">abc</p>' );
  915. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  916. observable.foo = 'baz';
  917. expect( el.outerHTML ).to.equal( '<p class="baz" custom="baz qux">abc</p>' );
  918. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  919. } );
  920. } );
  921. describe( 'if', () => {
  922. it( 'returns an object which describes the binding', () => {
  923. const spy = testUtils.sinon.spy();
  924. const binding = bind.if( 'foo', 'whenTrue', spy );
  925. expect( spy.called ).to.be.false;
  926. expect( binding ).to.have.keys( [ 'type', 'observable', 'emitter', 'attribute', 'callback', 'valueIfTrue' ] );
  927. expect( binding.observable ).to.equal( observable );
  928. expect( binding.callback ).to.equal( spy );
  929. expect( binding.attribute ).to.equal( 'foo' );
  930. expect( binding.valueIfTrue ).to.equal( 'whenTrue' );
  931. } );
  932. it( 'allows binding attribute to the observable – presence of an attribute (HTMLElement attribute)', () => {
  933. setElement( {
  934. tag: 'p',
  935. attributes: {
  936. 'class': bind.if( 'foo' )
  937. },
  938. children: [ 'abc' ]
  939. } );
  940. observable.foo = 'bar';
  941. expect( el.outerHTML ).to.equal( '<p class="true">abc</p>' );
  942. observable.foo = true;
  943. expect( el.outerHTML ).to.equal( '<p class="true">abc</p>' );
  944. observable.foo = 0;
  945. expect( el.outerHTML ).to.equal( '<p class="true">abc</p>' );
  946. observable.foo = false;
  947. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  948. observable.foo = null;
  949. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  950. observable.foo = undefined;
  951. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  952. observable.foo = '';
  953. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  954. } );
  955. // TODO: Is this alright? It makes sense but it's pretty useless. Text Node cannot be
  956. // removed just like an attribute of some HTMLElement.
  957. it( 'allows binding attribute to the observable – presence of an attribute (Text Node)', () => {
  958. setElement( {
  959. tag: 'p',
  960. children: [
  961. {
  962. text: bind.if( 'foo' )
  963. }
  964. ]
  965. } );
  966. observable.foo = 'abc';
  967. expect( el.outerHTML ).to.equal( '<p>true</p>' );
  968. observable.foo = true;
  969. expect( el.outerHTML ).to.equal( '<p>true</p>' );
  970. observable.foo = 0;
  971. expect( el.outerHTML ).to.equal( '<p>true</p>' );
  972. observable.foo = false;
  973. expect( el.outerHTML ).to.equal( '<p></p>' );
  974. observable.foo = null;
  975. expect( el.outerHTML ).to.equal( '<p></p>' );
  976. observable.foo = undefined;
  977. expect( el.outerHTML ).to.equal( '<p></p>' );
  978. observable.foo = '';
  979. expect( el.outerHTML ).to.equal( '<p></p>' );
  980. } );
  981. it( 'allows binding attribute to the observable – value of an attribute (HTMLElement attribute)', () => {
  982. setElement( {
  983. tag: 'p',
  984. attributes: {
  985. 'class': bind.if( 'foo', 'bar' )
  986. },
  987. children: [ 'abc' ]
  988. } );
  989. observable.foo = 'bar';
  990. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  991. observable.foo = true;
  992. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  993. observable.foo = 0;
  994. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  995. observable.foo = 64;
  996. expect( el.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  997. observable.foo = false;
  998. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  999. observable.foo = null;
  1000. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1001. observable.foo = undefined;
  1002. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1003. } );
  1004. it( 'allows binding attribute to the observable – value of an attribute (Text Node)', () => {
  1005. setElement( {
  1006. tag: 'p',
  1007. children: [
  1008. {
  1009. text: bind.if( 'foo', 'bar' )
  1010. }
  1011. ]
  1012. } );
  1013. observable.foo = 'bar';
  1014. expect( el.outerHTML ).to.equal( '<p>bar</p>' );
  1015. observable.foo = false;
  1016. expect( el.outerHTML ).to.equal( '<p></p>' );
  1017. observable.foo = 64;
  1018. expect( el.outerHTML ).to.equal( '<p>bar</p>' );
  1019. } );
  1020. it( 'allows binding attribute to the observable – array of bindings (HTMLElement attribute)', () => {
  1021. setElement( {
  1022. tag: 'p',
  1023. attributes: {
  1024. 'class': [
  1025. 'ck-class',
  1026. bind.if( 'foo', 'foo-set' ),
  1027. bind.if( 'bar', 'bar-not-set', ( value ) => !value ),
  1028. 'ck-end'
  1029. ]
  1030. },
  1031. children: [ 'abc' ]
  1032. } );
  1033. observable.foo = observable.bar = true;
  1034. expect( el.outerHTML ).to.equal( '<p class="ck-class foo-set ck-end">abc</p>' );
  1035. observable.foo = observable.bar = false;
  1036. expect( el.outerHTML ).to.equal( '<p class="ck-class bar-not-set ck-end">abc</p>' );
  1037. } );
  1038. it( 'allows binding attribute to the observable – value of an attribute processed by a callback', () => {
  1039. setElement( {
  1040. tag: 'p',
  1041. attributes: {
  1042. 'class': bind.if( 'foo', 'there–is–no–foo', value => !value )
  1043. },
  1044. children: [ 'abc' ]
  1045. } );
  1046. observable.foo = 'bar';
  1047. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1048. observable.foo = false;
  1049. expect( el.outerHTML ).to.equal( '<p class="there–is–no–foo">abc</p>' );
  1050. observable.foo = 64;
  1051. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1052. } );
  1053. it( 'allows binding attribute to the observable – value of an attribute processed by a callback (use Node)', () => {
  1054. setElement( {
  1055. tag: 'p',
  1056. attributes: {
  1057. 'class': bind.if( 'foo', 'eqls-tag-name', ( value, el ) => el.tagName === value )
  1058. },
  1059. children: [ 'abc' ]
  1060. } );
  1061. observable.foo = 'bar';
  1062. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1063. observable.foo = 'P';
  1064. expect( el.outerHTML ).to.equal( '<p class="eqls-tag-name">abc</p>' );
  1065. observable.foo = 64;
  1066. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1067. } );
  1068. it( 'allows binding attribute to the observable – falsy values', () => {
  1069. setElement( {
  1070. tag: 'p',
  1071. attributes: {
  1072. 'class': bind.if( 'foo', 'foo-is-set' )
  1073. },
  1074. children: [ 'abc' ]
  1075. } );
  1076. observable.foo = 'bar';
  1077. expect( el.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
  1078. observable.foo = true;
  1079. expect( el.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
  1080. observable.foo = 0;
  1081. expect( el.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
  1082. observable.foo = false;
  1083. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1084. observable.foo = null;
  1085. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1086. observable.foo = undefined;
  1087. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1088. observable.foo = '';
  1089. expect( el.outerHTML ).to.equal( '<p>abc</p>' );
  1090. } );
  1091. } );
  1092. it( 'works with Template#apply() – root element', () => {
  1093. new Template( {
  1094. tag: 'div',
  1095. attributes: {
  1096. class: bind.to( 'foo' )
  1097. }
  1098. } ).apply( el );
  1099. expect( el.getAttribute( 'class' ) ).to.equal( 'bar' );
  1100. observable.foo = 'baz';
  1101. expect( el.getAttribute( 'class' ) ).to.equal( 'baz' );
  1102. } );
  1103. it( 'works with Template#apply() – children', () => {
  1104. const el = document.createElement( 'div' );
  1105. const child = document.createElement( 'span' );
  1106. child.textContent = 'foo';
  1107. el.appendChild( child );
  1108. new Template( {
  1109. tag: 'div',
  1110. children: [
  1111. {
  1112. tag: 'span',
  1113. children: [
  1114. {
  1115. text: bind.to( 'foo' )
  1116. }
  1117. ]
  1118. }
  1119. ]
  1120. } ).apply( el );
  1121. expect( child.textContent ).to.equal( 'bar' );
  1122. observable.foo = 'baz';
  1123. expect( child.textContent ).to.equal( 'baz' );
  1124. } );
  1125. } );
  1126. } );
  1127. describe( 'extend', () => {
  1128. let observable, emitter, bind;
  1129. beforeEach( () => {
  1130. observable = new Model( {
  1131. foo: 'bar',
  1132. baz: 'qux'
  1133. } );
  1134. emitter = Object.create( DOMEmitterMixin );
  1135. bind = Template.bind( observable, emitter );
  1136. } );
  1137. it( 'does not modify passed definition', () => {
  1138. const def = {
  1139. tag: 'p',
  1140. attributes: {
  1141. a: 'foo',
  1142. }
  1143. };
  1144. const ext = {
  1145. attributes: {
  1146. b: 'bar'
  1147. }
  1148. };
  1149. const tpl = new Template( def );
  1150. Template.extend( tpl, ext );
  1151. expect( def.attributes.a ).to.equal( 'foo' );
  1152. expect( ext.attributes.b ).to.equal( 'bar' );
  1153. expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
  1154. expect( tpl.definition.attributes.b[ 0 ] ).to.equal( 'bar' );
  1155. } );
  1156. describe( 'attributes', () => {
  1157. it( 'extends existing - simple', () => {
  1158. extensionTest(
  1159. {
  1160. tag: 'p',
  1161. attributes: {
  1162. a: 'b'
  1163. }
  1164. },
  1165. {
  1166. attributes: {
  1167. a: 'c'
  1168. }
  1169. },
  1170. '<p a="b c"></p>'
  1171. );
  1172. } );
  1173. it( 'extends existing - complex #1', () => {
  1174. extensionTest(
  1175. {
  1176. tag: 'p',
  1177. attributes: {
  1178. a: [ 'b', 'c' ]
  1179. }
  1180. },
  1181. {
  1182. attributes: {
  1183. a: 'd'
  1184. }
  1185. },
  1186. '<p a="b c d"></p>'
  1187. );
  1188. } );
  1189. it( 'extends existing - complex #2', () => {
  1190. extensionTest(
  1191. {
  1192. tag: 'p',
  1193. attributes: {
  1194. a: {
  1195. value: 'b'
  1196. }
  1197. }
  1198. },
  1199. {
  1200. attributes: {
  1201. a: [ 'c', 'd' ]
  1202. }
  1203. },
  1204. '<p a="b c d"></p>'
  1205. );
  1206. } );
  1207. it( 'extends existing - complex #3', () => {
  1208. extensionTest(
  1209. {
  1210. tag: 'p',
  1211. attributes: {
  1212. a: [ 'b' ]
  1213. }
  1214. },
  1215. {
  1216. attributes: {
  1217. a: [ 'c', 'd' ]
  1218. }
  1219. },
  1220. '<p a="b c d"></p>'
  1221. );
  1222. } );
  1223. it( 'extends existing - bindings #1', () => {
  1224. const el = extensionTest(
  1225. {
  1226. tag: 'p',
  1227. attributes: {
  1228. a: bind.to( 'foo' )
  1229. }
  1230. },
  1231. {
  1232. attributes: {
  1233. a: [ 'c', 'd' ]
  1234. }
  1235. },
  1236. '<p a="bar c d"></p>'
  1237. );
  1238. observable.foo = 'baz';
  1239. expect( el.outerHTML ).to.equal( '<p a="baz c d"></p>' );
  1240. } );
  1241. it( 'extends existing - bindings #2', () => {
  1242. const el = extensionTest(
  1243. {
  1244. tag: 'p',
  1245. attributes: {
  1246. a: [ 'b', bind.to( 'foo' ) ]
  1247. }
  1248. },
  1249. {
  1250. attributes: {
  1251. a: [ 'c', bind.to( 'baz' ) ]
  1252. }
  1253. },
  1254. '<p a="b bar c qux"></p>'
  1255. );
  1256. observable.foo = 'abc';
  1257. observable.baz = 'def';
  1258. expect( el.outerHTML ).to.equal( '<p a="b abc c def"></p>' );
  1259. } );
  1260. it( 'creates new - no attributes', () => {
  1261. extensionTest(
  1262. {
  1263. tag: 'p',
  1264. },
  1265. {
  1266. attributes: {
  1267. c: 'd'
  1268. }
  1269. },
  1270. '<p c="d"></p>'
  1271. );
  1272. } );
  1273. it( 'creates new - simple', () => {
  1274. extensionTest(
  1275. {
  1276. tag: 'p',
  1277. attributes: {
  1278. a: 'b'
  1279. }
  1280. },
  1281. {
  1282. attributes: {
  1283. c: 'd'
  1284. }
  1285. },
  1286. '<p a="b" c="d"></p>'
  1287. );
  1288. } );
  1289. it( 'creates new - array', () => {
  1290. extensionTest(
  1291. {
  1292. tag: 'p',
  1293. attributes: {
  1294. a: 'b'
  1295. }
  1296. },
  1297. {
  1298. attributes: {
  1299. c: [ 'd', 'e' ]
  1300. }
  1301. },
  1302. '<p a="b" c="d e"></p>'
  1303. );
  1304. } );
  1305. it( 'creates new - bindings #1', () => {
  1306. const el = extensionTest(
  1307. {
  1308. tag: 'p',
  1309. attributes: {
  1310. a: 'b'
  1311. }
  1312. },
  1313. {
  1314. attributes: {
  1315. c: bind.to( 'foo' )
  1316. }
  1317. },
  1318. '<p a="b" c="bar"></p>'
  1319. );
  1320. observable.foo = 'abc';
  1321. expect( el.outerHTML ).to.equal( '<p a="b" c="abc"></p>' );
  1322. } );
  1323. it( 'creates new - bindings #2', () => {
  1324. const el = extensionTest(
  1325. {
  1326. tag: 'p',
  1327. attributes: {
  1328. a: 'b'
  1329. }
  1330. },
  1331. {
  1332. attributes: {
  1333. c: [ 'd', bind.to( 'foo' ) ]
  1334. }
  1335. },
  1336. '<p a="b" c="d bar"></p>'
  1337. );
  1338. observable.foo = 'abc';
  1339. expect( el.outerHTML ).to.equal( '<p a="b" c="d abc"></p>' );
  1340. } );
  1341. } );
  1342. describe( 'text', () => {
  1343. it( 'extends existing - simple', () => {
  1344. const el = extensionTest(
  1345. {
  1346. tag: 'p',
  1347. children: [
  1348. 'foo'
  1349. ]
  1350. },
  1351. {
  1352. children: [
  1353. 'bar'
  1354. ]
  1355. },
  1356. '<p>foobar</p>'
  1357. );
  1358. expect( el.childNodes ).to.have.length( 1 );
  1359. } );
  1360. it( 'extends existing - complex #1', () => {
  1361. const el = extensionTest(
  1362. {
  1363. tag: 'p',
  1364. children: [
  1365. { text: 'foo' }
  1366. ]
  1367. },
  1368. {
  1369. children: [
  1370. 'bar'
  1371. ]
  1372. },
  1373. '<p>foobar</p>'
  1374. );
  1375. expect( el.childNodes ).to.have.length( 1 );
  1376. } );
  1377. it( 'extends existing - complex #2', () => {
  1378. const el = extensionTest(
  1379. {
  1380. tag: 'p',
  1381. children: [
  1382. { text: 'foo' }
  1383. ]
  1384. },
  1385. {
  1386. children: [
  1387. { text: 'bar' }
  1388. ]
  1389. },
  1390. '<p>foobar</p>'
  1391. );
  1392. expect( el.childNodes ).to.have.length( 1 );
  1393. } );
  1394. it( 'extends existing - bindings #1', () => {
  1395. const el = extensionTest(
  1396. {
  1397. tag: 'p',
  1398. children: [
  1399. { text: bind.to( 'foo' ) }
  1400. ]
  1401. },
  1402. {
  1403. children: [
  1404. 'abc'
  1405. ]
  1406. },
  1407. '<p>bar abc</p>'
  1408. );
  1409. observable.foo = 'asd';
  1410. expect( el.outerHTML ).to.equal( '<p>asd abc</p>' );
  1411. expect( el.childNodes ).to.have.length( 1 );
  1412. } );
  1413. it( 'extends existing - bindings #2', () => {
  1414. const el = extensionTest(
  1415. {
  1416. tag: 'p',
  1417. children: [
  1418. 'abc'
  1419. ]
  1420. },
  1421. {
  1422. children: [
  1423. { text: bind.to( 'foo' ) }
  1424. ]
  1425. },
  1426. '<p>abc bar</p>'
  1427. );
  1428. observable.foo = 'asd';
  1429. expect( el.outerHTML ).to.equal( '<p>abc asd</p>' );
  1430. expect( el.childNodes ).to.have.length( 1 );
  1431. } );
  1432. it( 'extends existing - bindings #3', () => {
  1433. const el = extensionTest(
  1434. {
  1435. tag: 'p',
  1436. children: [
  1437. { text: bind.to( 'foo' ) },
  1438. 'X'
  1439. ]
  1440. },
  1441. {
  1442. children: [
  1443. { text: bind.to( 'baz' ) },
  1444. 'Y'
  1445. ]
  1446. },
  1447. '<p>bar quxXY</p>'
  1448. );
  1449. observable.foo = 'A';
  1450. observable.baz = 'B';
  1451. expect( el.outerHTML ).to.equal( '<p>A BXY</p>' );
  1452. expect( el.childNodes ).to.have.length( 2 );
  1453. } );
  1454. } );
  1455. describe( 'children', () => {
  1456. it( 'should throw when the number of children does not correspond', () => {
  1457. expect( () => {
  1458. extensionTest(
  1459. {
  1460. tag: 'p',
  1461. children: [
  1462. 'foo'
  1463. ]
  1464. },
  1465. {
  1466. children: [
  1467. 'foo',
  1468. 'bar'
  1469. ]
  1470. },
  1471. 'it should fail'
  1472. );
  1473. } ).to.throwCKEditorError( /ui-template-extend-children-mismatch/ );
  1474. } );
  1475. it( 'should throw when no children in target but extending one', () => {
  1476. expect( () => {
  1477. extensionTest(
  1478. {
  1479. tag: 'p',
  1480. },
  1481. {
  1482. children: [
  1483. {
  1484. tag: 'b'
  1485. }
  1486. ]
  1487. },
  1488. 'it should fail'
  1489. );
  1490. } ).to.throwCKEditorError( /ui-template-extend-children-mismatch/ );
  1491. } );
  1492. it( 'should throw when the number of children does not correspond on some deeper level', () => {
  1493. expect( () => {
  1494. extensionTest(
  1495. {
  1496. tag: 'p',
  1497. children: [
  1498. {
  1499. tag: 'span',
  1500. attributes: {
  1501. class: 'A'
  1502. },
  1503. children: [
  1504. 'A',
  1505. {
  1506. tag: 'span',
  1507. attributes: {
  1508. class: 'AA'
  1509. },
  1510. children: [
  1511. 'AA'
  1512. ]
  1513. }
  1514. ]
  1515. }
  1516. ]
  1517. },
  1518. {
  1519. children: [
  1520. {
  1521. attributes: {
  1522. class: 'B'
  1523. },
  1524. children: [
  1525. 'B'
  1526. ]
  1527. }
  1528. ]
  1529. },
  1530. 'it should fail'
  1531. );
  1532. } ).to.throwCKEditorError( /ui-template-extend-children-mismatch/ );
  1533. } );
  1534. it( 'extends existing - simple', () => {
  1535. extensionTest(
  1536. {
  1537. tag: 'p',
  1538. children: [
  1539. {
  1540. tag: 'span',
  1541. attributes: {
  1542. class: 'foo'
  1543. }
  1544. }
  1545. ]
  1546. },
  1547. {
  1548. children: [
  1549. {
  1550. tag: 'span',
  1551. attributes: {
  1552. class: 'bar'
  1553. }
  1554. }
  1555. ]
  1556. },
  1557. '<p><span class="foo bar"></span></p>'
  1558. );
  1559. } );
  1560. it( 'extends existing - complex', () => {
  1561. extensionTest(
  1562. {
  1563. tag: 'p',
  1564. children: [
  1565. {
  1566. tag: 'span',
  1567. attributes: {
  1568. class: 'A'
  1569. },
  1570. children: [
  1571. 'A',
  1572. {
  1573. tag: 'span',
  1574. attributes: {
  1575. class: 'AA'
  1576. },
  1577. children: [
  1578. 'AA'
  1579. ]
  1580. }
  1581. ]
  1582. }
  1583. ]
  1584. },
  1585. {
  1586. children: [
  1587. {
  1588. tag: 'span',
  1589. attributes: {
  1590. class: 'B'
  1591. },
  1592. children: [
  1593. 'B',
  1594. {
  1595. tag: 'span',
  1596. attributes: {
  1597. class: 'BB'
  1598. },
  1599. children: [
  1600. 'BB'
  1601. ]
  1602. }
  1603. ]
  1604. }
  1605. ]
  1606. },
  1607. '<p><span class="A B">AB<span class="AA BB">AABB</span></span></p>'
  1608. );
  1609. } );
  1610. it( 'allows extending a particular child', () => {
  1611. const template = new Template( {
  1612. tag: 'p',
  1613. children: [
  1614. {
  1615. tag: 'span',
  1616. attributes: {
  1617. class: 'foo'
  1618. }
  1619. }
  1620. ]
  1621. } );
  1622. Template.extend( template.definition.children[ 0 ], {
  1623. attributes: {
  1624. class: 'bar'
  1625. }
  1626. } );
  1627. expect( template.render().outerHTML ).to.equal( '<p><span class="foo bar"></span></p>' );
  1628. } );
  1629. it( 'allows extending a particular child – recursively', () => {
  1630. const template = new Template( {
  1631. tag: 'p',
  1632. children: [
  1633. {
  1634. tag: 'span',
  1635. attributes: {
  1636. class: 'A'
  1637. },
  1638. children: [
  1639. 'A',
  1640. {
  1641. tag: 'span',
  1642. attributes: {
  1643. class: 'AA'
  1644. },
  1645. children: [
  1646. 'AA'
  1647. ]
  1648. }
  1649. ]
  1650. }
  1651. ]
  1652. } );
  1653. Template.extend( template.definition.children[ 0 ], {
  1654. attributes: {
  1655. class: 'B',
  1656. },
  1657. children: [
  1658. 'B',
  1659. {
  1660. attributes: {
  1661. class: 'BB'
  1662. }
  1663. }
  1664. ]
  1665. } );
  1666. expect( template.render().outerHTML ).to.equal( '<p><span class="A B">AB<span class="AA BB">AA</span></span></p>' );
  1667. } );
  1668. } );
  1669. describe( 'listeners', () => {
  1670. it( 'extends existing', () => {
  1671. const spy1 = testUtils.sinon.spy();
  1672. const spy2 = testUtils.sinon.spy();
  1673. const spy3 = testUtils.sinon.spy();
  1674. const spy4 = testUtils.sinon.spy();
  1675. const spy5 = testUtils.sinon.spy();
  1676. observable.on( 'A', spy1 );
  1677. observable.on( 'C', spy2 );
  1678. observable.on( 'B', spy3 );
  1679. observable.on( 'D', spy4 );
  1680. const el = extensionTest(
  1681. {
  1682. tag: 'p',
  1683. children: [
  1684. {
  1685. tag: 'span'
  1686. }
  1687. ],
  1688. on: {
  1689. click: bind.to( 'A' ),
  1690. 'click@span': [
  1691. bind.to( 'B' ),
  1692. bind.to( spy5 )
  1693. ]
  1694. }
  1695. },
  1696. {
  1697. on: {
  1698. click: bind.to( 'C' ),
  1699. 'click@span': bind.to( 'D' )
  1700. }
  1701. },
  1702. '<p><span></span></p>'
  1703. );
  1704. dispatchEvent( el, 'click' );
  1705. expect( spy1.calledOnce ).to.be.true;
  1706. expect( spy2.calledOnce ).to.be.true;
  1707. expect( spy3.called ).to.be.false;
  1708. expect( spy4.called ).to.be.false;
  1709. expect( spy5.called ).to.be.false;
  1710. dispatchEvent( el.firstChild, 'click' );
  1711. expect( spy1.calledTwice ).to.be.true;
  1712. expect( spy2.calledTwice ).to.be.true;
  1713. expect( spy3.calledOnce ).to.be.true;
  1714. expect( spy4.calledOnce ).to.be.true;
  1715. expect( spy5.calledOnce ).to.be.true;
  1716. } );
  1717. it( 'creates new', () => {
  1718. const spy1 = testUtils.sinon.spy();
  1719. const spy2 = testUtils.sinon.spy();
  1720. const spy3 = testUtils.sinon.spy();
  1721. observable.on( 'A', spy1 );
  1722. observable.on( 'B', spy2 );
  1723. const el = extensionTest(
  1724. {
  1725. tag: 'p',
  1726. children: [
  1727. {
  1728. tag: 'span'
  1729. }
  1730. ],
  1731. },
  1732. {
  1733. on: {
  1734. click: bind.to( 'A' ),
  1735. 'click@span': [
  1736. bind.to( 'B' ),
  1737. bind.to( spy3 )
  1738. ]
  1739. }
  1740. },
  1741. '<p><span></span></p>'
  1742. );
  1743. dispatchEvent( el, 'click' );
  1744. expect( spy1.calledOnce ).to.be.true;
  1745. expect( spy2.called ).to.be.false;
  1746. expect( spy3.called ).to.be.false;
  1747. dispatchEvent( el.firstChild, 'click' );
  1748. expect( spy1.calledTwice ).to.be.true;
  1749. expect( spy2.calledOnce ).to.be.true;
  1750. expect( spy3.calledOnce ).to.be.true;
  1751. } );
  1752. } );
  1753. } );
  1754. } );
  1755. function setElement( template ) {
  1756. el = new Template( template ).render();
  1757. document.body.appendChild( el );
  1758. }
  1759. function extensionTest( base, extension, expectedHtml ) {
  1760. const template = new Template( base );
  1761. Template.extend( template, extension );
  1762. const el = template.render();
  1763. document.body.appendChild( el );
  1764. expect( el.outerHTML ).to.equal( expectedHtml );
  1765. return el;
  1766. }
  1767. function dispatchEvent( el, domEvtName ) {
  1768. if ( !el.parentNode ) {
  1769. throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
  1770. }
  1771. el.dispatchEvent( new Event( domEvtName, {
  1772. bubbles: true
  1773. } ) );
  1774. }