template.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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 CKEditorError from '/ckeditor5/utils/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. it( 'uses DOM updater – attributes', () => {
  256. const spy = testUtils.sinon.spy();
  257. const el = new Template( {
  258. tag: 'p',
  259. attributes: {
  260. 'class': {}
  261. },
  262. _modelBinders: {
  263. attributes: {
  264. class: spy
  265. }
  266. }
  267. } ).render();
  268. // Check whether DOM updater is correct.
  269. spy.firstCall.args[ 1 ].set( 'x' );
  270. expect( el.outerHTML ).to.be.equal( '<p class="x"></p>' );
  271. spy.firstCall.args[ 1 ].remove();
  272. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  273. } );
  274. it( 'uses DOM updater – text', () => {
  275. const spy = testUtils.sinon.spy();
  276. const el = new Template( {
  277. tag: 'p',
  278. children: [
  279. {
  280. text: {},
  281. _modelBinders: {
  282. text: spy
  283. }
  284. }
  285. ],
  286. } ).render();
  287. // Check whether DOM updater is correct.
  288. spy.firstCall.args[ 1 ].set( 'x' );
  289. expect( el.outerHTML ).to.be.equal( '<p>x</p>' );
  290. spy.firstCall.args[ 1 ].remove();
  291. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  292. } );
  293. } );
  294. describe( 'apply', () => {
  295. beforeEach( () => {
  296. el = document.createElement( 'div' );
  297. text = document.createTextNode( '' );
  298. } );
  299. it( 'throws when wrong template definition', () => {
  300. expect( () => {
  301. new Template( {
  302. tag: 'p',
  303. text: 'foo'
  304. } ).apply( el );
  305. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  306. } );
  307. it( 'throws when no HTMLElement passed', () => {
  308. expect( () => {
  309. new Template( {
  310. tag: 'p'
  311. } ).apply();
  312. } ).to.throw( CKEditorError, /ui-template-wrong-node/ );
  313. } );
  314. it( 'accepts empty template definition', () => {
  315. new Template( {} ).apply( el );
  316. new Template( {} ).apply( text );
  317. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  318. expect( text.textContent ).to.be.equal( '' );
  319. } );
  320. it( 'applies textContent to a Text Node', () => {
  321. new Template( {
  322. text: 'abc'
  323. } ).apply( text );
  324. expect( text.textContent ).to.be.equal( 'abc' );
  325. } );
  326. it( 'applies attributes to an HTMLElement', () => {
  327. new Template( {
  328. tag: 'div',
  329. attributes: {
  330. 'class': [ 'a', 'b' ],
  331. x: 'bar'
  332. }
  333. } ).apply( el );
  334. expect( el.outerHTML ).to.be.equal( '<div class="a b" x="bar"></div>' );
  335. } );
  336. it( 'applies doesn\'t apply new child to an HTMLElement – Text Node', () => {
  337. new Template( {
  338. tag: 'div',
  339. children: [ 'foo' ]
  340. } ).apply( el );
  341. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  342. } );
  343. it( 'applies doesn\'t apply new child to an HTMLElement – HTMLElement', () => {
  344. new Template( {
  345. tag: 'div',
  346. children: [
  347. {
  348. tag: 'span'
  349. }
  350. ]
  351. } ).apply( el );
  352. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  353. } );
  354. it( 'applies new textContent to an existing Text Node of an HTMLElement', () => {
  355. el.textContent = 'bar';
  356. new Template( {
  357. tag: 'div',
  358. children: [ 'foo' ]
  359. } ).apply( el );
  360. expect( el.outerHTML ).to.be.equal( '<div>foo</div>' );
  361. } );
  362. it( 'applies attributes and TextContent to a DOM tree', () => {
  363. el.textContent = 'abc';
  364. el.appendChild( document.createElement( 'span' ) );
  365. new Template( {
  366. tag: 'div',
  367. attributes: {
  368. 'class': 'parent'
  369. },
  370. children: [
  371. 'Children: ',
  372. {
  373. tag: 'span',
  374. attributes: {
  375. class: 'child'
  376. }
  377. }
  378. ]
  379. } ).apply( el );
  380. expect( el.outerHTML ).to.be.equal( '<div class="parent">Children: <span class="child"></span></div>' );
  381. } );
  382. it( 'activates listener attachers – root', () => {
  383. const spy = testUtils.sinon.spy();
  384. new Template( {
  385. tag: 'div',
  386. on: {
  387. _listenerAttachers: {
  388. click: spy
  389. }
  390. }
  391. } ).apply( el );
  392. sinon.assert.calledWithExactly( spy, el, 'click', null );
  393. } );
  394. it( 'activates listener attachers – children', () => {
  395. const spy = testUtils.sinon.spy();
  396. el.appendChild( document.createElement( 'span' ) );
  397. new Template( {
  398. tag: 'div',
  399. children: [
  400. {
  401. tag: 'span',
  402. on: {
  403. _listenerAttachers: {
  404. click: spy
  405. }
  406. }
  407. }
  408. ]
  409. } ).apply( el );
  410. sinon.assert.calledWithExactly( spy, el.firstChild, 'click', null );
  411. } );
  412. it( 'activates model bindings – root', () => {
  413. const spy = testUtils.sinon.spy();
  414. new Template( {
  415. tag: 'div',
  416. attributes: {
  417. class: {}
  418. },
  419. _modelBinders: {
  420. attributes: {
  421. class: spy
  422. }
  423. }
  424. } ).apply( el );
  425. sinon.assert.calledWithExactly( spy, el, sinon.match.object );
  426. } );
  427. it( 'activates model bindings – children', () => {
  428. const spy = testUtils.sinon.spy();
  429. el.appendChild( document.createElement( 'span' ) );
  430. new Template( {
  431. tag: 'div',
  432. children: [
  433. {
  434. tag: 'span',
  435. attributes: {
  436. class: {}
  437. },
  438. _modelBinders: {
  439. attributes: {
  440. class: spy
  441. }
  442. }
  443. }
  444. ]
  445. } ).apply( el );
  446. sinon.assert.calledWithExactly( spy, el.firstChild, sinon.match.object );
  447. } );
  448. } );
  449. } );