template.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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/_utils/utils.js';
  9. import Template from '/ckeditor5/core/ui/template.js';
  10. import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
  11. testUtils.createSinonSandbox();
  12. let el, text;
  13. describe( 'Template', () => {
  14. describe( 'constructor', () => {
  15. it( 'accepts the definition', () => {
  16. const def = {
  17. tag: 'p'
  18. };
  19. expect( new Template( def ).definition ).to.equal( def );
  20. } );
  21. } );
  22. describe( 'render', () => {
  23. it( 'throws when wrong template definition', () => {
  24. expect( () => {
  25. new Template( {} ).render();
  26. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  27. expect( () => {
  28. new Template( {
  29. tag: 'p',
  30. text: 'foo'
  31. } ).render();
  32. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  33. } );
  34. it( 'creates a HTMLElement', () => {
  35. const el = new Template( {
  36. tag: 'p',
  37. } ).render();
  38. expect( el ).to.be.instanceof( HTMLElement );
  39. expect( el.parentNode ).to.be.null;
  40. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  41. } );
  42. it( 'renders HTMLElement attributes', () => {
  43. const el = new Template( {
  44. tag: 'p',
  45. attributes: {
  46. 'class': [ 'a', 'b' ],
  47. x: 'bar'
  48. },
  49. children: [ 'foo' ]
  50. } ).render();
  51. expect( el ).to.be.instanceof( HTMLElement );
  52. expect( el.parentNode ).to.be.null;
  53. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  54. } );
  55. it( 'renders HTMLElement attributes – empty', () => {
  56. const el = new Template( {
  57. tag: 'p',
  58. attributes: {
  59. class: '',
  60. x: [ '', '' ]
  61. },
  62. children: [ 'foo' ]
  63. } ).render();
  64. expect( el.outerHTML ).to.be.equal( '<p class="" x="">foo</p>' );
  65. } );
  66. it( 'renders HTMLElement attributes – falsy values', () => {
  67. const el = new Template( {
  68. tag: 'p',
  69. attributes: {
  70. class: false,
  71. x: [ '', null ]
  72. },
  73. children: [ 'foo' ]
  74. } ).render();
  75. expect( el.outerHTML ).to.be.equal( '<p class="false" x="null">foo</p>' );
  76. } );
  77. it( 'creates HTMLElement children', () => {
  78. const el = new Template( {
  79. tag: 'p',
  80. attributes: {
  81. a: 'A'
  82. },
  83. children: [
  84. {
  85. tag: 'b',
  86. children: [ 'B' ]
  87. },
  88. {
  89. tag: 'i',
  90. children: [
  91. 'C',
  92. {
  93. tag: 'b',
  94. children: [ 'D' ]
  95. }
  96. ]
  97. }
  98. ]
  99. } ).render();
  100. expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
  101. } );
  102. it( 'creates a Text node', () => {
  103. const node = new Template( { text: 'foo' } ).render();
  104. expect( node.nodeType ).to.be.equal( 3 );
  105. expect( node.textContent ).to.be.equal( 'foo' );
  106. } );
  107. it( 'creates a child Text Node (different syntaxes)', () => {
  108. const el = new Template( {
  109. tag: 'p',
  110. children: [
  111. 'foo',
  112. { text: 'bar' }
  113. ]
  114. } ).render();
  115. expect( el.outerHTML ).to.be.equal( '<p>foobar</p>' );
  116. } );
  117. it( 'creates multiple child Text Nodes', () => {
  118. const el = new Template( {
  119. tag: 'p',
  120. children: [ 'a', 'b', { text: 'c' }, 'd' ]
  121. } ).render();
  122. expect( el.childNodes ).to.have.length( 4 );
  123. expect( el.outerHTML ).to.be.equal( '<p>abcd</p>' );
  124. } );
  125. it( 'activates listener attachers – root', () => {
  126. const spy1 = testUtils.sinon.spy();
  127. const spy2 = testUtils.sinon.spy();
  128. const spy3 = testUtils.sinon.spy();
  129. const el = new Template( {
  130. tag: 'p',
  131. on: {
  132. _listenerAttachers: {
  133. foo: spy1,
  134. baz: [ spy2, spy3 ]
  135. }
  136. }
  137. } ).render();
  138. sinon.assert.calledWithExactly( spy1, el, 'foo', null );
  139. sinon.assert.calledWithExactly( spy2, el, 'baz', null );
  140. sinon.assert.calledWithExactly( spy3, el, 'baz', null );
  141. } );
  142. it( 'activates listener attachers – children', () => {
  143. const spy = testUtils.sinon.spy();
  144. const el = new Template( {
  145. tag: 'p',
  146. children: [
  147. {
  148. tag: 'span',
  149. on: {
  150. _listenerAttachers: {
  151. bar: spy
  152. }
  153. }
  154. }
  155. ],
  156. } ).render();
  157. sinon.assert.calledWithExactly( spy, el.firstChild, 'bar', null );
  158. } );
  159. it( 'activates listener attachers – DOM selectors', () => {
  160. const spy1 = testUtils.sinon.spy();
  161. const spy2 = testUtils.sinon.spy();
  162. const spy3 = testUtils.sinon.spy();
  163. const spy4 = testUtils.sinon.spy();
  164. const el = new Template( {
  165. tag: 'p',
  166. children: [
  167. {
  168. tag: 'span',
  169. attributes: {
  170. 'id': 'x'
  171. }
  172. },
  173. {
  174. tag: 'span',
  175. attributes: {
  176. 'class': 'y'
  177. },
  178. on: {
  179. _listenerAttachers: {
  180. 'bar@p': spy2
  181. }
  182. }
  183. },
  184. ],
  185. on: {
  186. _listenerAttachers: {
  187. 'foo@span': spy1,
  188. 'baz@.y': [ spy3, spy4 ]
  189. }
  190. }
  191. } ).render();
  192. sinon.assert.calledWithExactly( spy1, el, 'foo', 'span' );
  193. sinon.assert.calledWithExactly( spy2, el.lastChild, 'bar', 'p' );
  194. sinon.assert.calledWithExactly( spy3, el, 'baz', '.y' );
  195. sinon.assert.calledWithExactly( spy4, el, 'baz', '.y' );
  196. } );
  197. it( 'activates model bindings – attributes', () => {
  198. const spy1 = testUtils.sinon.spy();
  199. const spy2 = testUtils.sinon.spy();
  200. const el = new Template( {
  201. tag: 'p',
  202. attributes: {
  203. 'class': {}
  204. },
  205. children: [
  206. {
  207. tag: 'span',
  208. attributes: {
  209. id: {}
  210. },
  211. _modelBinders: {
  212. attributes: {
  213. id: spy2
  214. }
  215. }
  216. }
  217. ],
  218. _modelBinders: {
  219. attributes: {
  220. class: spy1
  221. }
  222. }
  223. } ).render();
  224. sinon.assert.calledWithExactly( spy1, el, sinon.match.object );
  225. sinon.assert.calledWithExactly( spy2, el.firstChild, sinon.match.object );
  226. } );
  227. it( 'activates model bindings – Text Node', () => {
  228. const spy1 = testUtils.sinon.spy();
  229. const spy2 = testUtils.sinon.spy();
  230. const el = new Template( {
  231. tag: 'p',
  232. children: [
  233. {
  234. text: {},
  235. _modelBinders: {
  236. text: spy1
  237. }
  238. },
  239. {
  240. tag: 'span',
  241. children: [
  242. {
  243. text: {},
  244. _modelBinders: {
  245. text: spy2
  246. }
  247. }
  248. ]
  249. }
  250. ]
  251. } ).render();
  252. sinon.assert.calledWithExactly( spy1, el.firstChild, sinon.match.object );
  253. sinon.assert.calledWithExactly( spy2, el.lastChild.firstChild, sinon.match.object );
  254. } );
  255. } );
  256. describe( 'apply', () => {
  257. beforeEach( () => {
  258. el = document.createElement( 'div' );
  259. text = document.createTextNode( '' );
  260. } );
  261. it( 'throws when wrong template definition', () => {
  262. expect( () => {
  263. new Template( {
  264. tag: 'p',
  265. text: 'foo'
  266. } ).apply( el );
  267. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  268. } );
  269. it( 'throws when no HTMLElement passed', () => {
  270. expect( () => {
  271. new Template( {
  272. tag: 'p'
  273. } ).apply();
  274. } ).to.throw( CKEditorError, /ui-template-wrong-node/ );
  275. } );
  276. it( 'accepts empty template definition', () => {
  277. new Template( {} ).apply( el );
  278. new Template( {} ).apply( text );
  279. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  280. expect( text.textContent ).to.be.equal( '' );
  281. } );
  282. it( 'applies textContent to a Text Node', () => {
  283. new Template( {
  284. text: 'abc'
  285. } ).apply( text );
  286. expect( text.textContent ).to.be.equal( 'abc' );
  287. } );
  288. it( 'applies attributes to an HTMLElement', () => {
  289. new Template( {
  290. tag: 'div',
  291. attributes: {
  292. 'class': [ 'a', 'b' ],
  293. x: 'bar'
  294. }
  295. } ).apply( el );
  296. expect( el.outerHTML ).to.be.equal( '<div class="a b" x="bar"></div>' );
  297. } );
  298. it( 'applies doesn\'t apply new child to an HTMLElement – Text Node', () => {
  299. new Template( {
  300. tag: 'div',
  301. children: [ 'foo' ]
  302. } ).apply( el );
  303. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  304. } );
  305. it( 'applies doesn\'t apply new child to an HTMLElement – HTMLElement', () => {
  306. new Template( {
  307. tag: 'div',
  308. children: [
  309. {
  310. tag: 'span'
  311. }
  312. ]
  313. } ).apply( el );
  314. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  315. } );
  316. it( 'applies new textContent to an existing Text Node of an HTMLElement', () => {
  317. el.textContent = 'bar';
  318. new Template( {
  319. tag: 'div',
  320. children: [ 'foo' ]
  321. } ).apply( el );
  322. expect( el.outerHTML ).to.be.equal( '<div>foo</div>' );
  323. } );
  324. it( 'applies attributes and TextContent to a DOM tree', () => {
  325. el.textContent = 'abc';
  326. el.appendChild( document.createElement( 'span' ) );
  327. new Template( {
  328. tag: 'div',
  329. attributes: {
  330. 'class': 'parent'
  331. },
  332. children: [
  333. 'Children: ',
  334. {
  335. tag: 'span',
  336. attributes: {
  337. class: 'child'
  338. }
  339. }
  340. ]
  341. } ).apply( el );
  342. expect( el.outerHTML ).to.be.equal( '<div class="parent">Children: <span class="child"></span></div>' );
  343. } );
  344. it( 'activates listener attachers – root', () => {
  345. const spy = testUtils.sinon.spy();
  346. new Template( {
  347. tag: 'div',
  348. on: {
  349. _listenerAttachers: {
  350. click: spy
  351. }
  352. }
  353. } ).apply( el );
  354. sinon.assert.calledWithExactly( spy, el, 'click', null );
  355. } );
  356. it( 'activates listener attachers – children', () => {
  357. const spy = testUtils.sinon.spy();
  358. el.appendChild( document.createElement( 'span' ) );
  359. new Template( {
  360. tag: 'div',
  361. children: [
  362. {
  363. tag: 'span',
  364. on: {
  365. _listenerAttachers: {
  366. click: spy
  367. }
  368. }
  369. }
  370. ]
  371. } ).apply( el );
  372. sinon.assert.calledWithExactly( spy, el.firstChild, 'click', null );
  373. } );
  374. it( 'activates model bindings – root', () => {
  375. const spy = testUtils.sinon.spy();
  376. new Template( {
  377. tag: 'div',
  378. attributes: {
  379. class: {}
  380. },
  381. _modelBinders: {
  382. attributes: {
  383. class: spy
  384. }
  385. }
  386. } ).apply( el );
  387. sinon.assert.calledWithExactly( spy, el, sinon.match.object );
  388. } );
  389. it( 'activates model bindings – children', () => {
  390. const spy = testUtils.sinon.spy();
  391. el.appendChild( document.createElement( 'span' ) );
  392. new Template( {
  393. tag: 'div',
  394. children: [
  395. {
  396. tag: 'span',
  397. attributes: {
  398. class: {}
  399. },
  400. _modelBinders: {
  401. attributes: {
  402. class: spy
  403. }
  404. }
  405. }
  406. ]
  407. } ).apply( el );
  408. sinon.assert.calledWithExactly( spy, el.firstChild, sinon.match.object );
  409. } );
  410. } );
  411. } );