template.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  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. expect( el.namespaceURI ).to.be.equal( 'http://www.w3.org/1999/xhtml' );
  42. } );
  43. it( 'creates an element in a custom namespace', () => {
  44. const el = new Template( {
  45. tag: 'p',
  46. ns: 'foo'
  47. } ).render();
  48. expect( el.namespaceURI ).to.be.equal( 'foo' );
  49. } );
  50. it( 'renders HTMLElement attributes', () => {
  51. const el = new Template( {
  52. tag: 'p',
  53. attributes: {
  54. 'class': [ 'a', 'b' ],
  55. x: 'bar'
  56. },
  57. children: [ 'foo' ]
  58. } ).render();
  59. expect( el ).to.be.instanceof( HTMLElement );
  60. expect( el.parentNode ).to.be.null;
  61. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  62. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  63. } );
  64. it( 'renders HTMLElement attributes – empty', () => {
  65. const el = new Template( {
  66. tag: 'p',
  67. attributes: {
  68. class: '',
  69. x: [ '', '' ]
  70. },
  71. children: [ 'foo' ]
  72. } ).render();
  73. expect( el.outerHTML ).to.be.equal( '<p class="" x="">foo</p>' );
  74. } );
  75. it( 'renders HTMLElement attributes – falsy values', () => {
  76. const el = new Template( {
  77. tag: 'p',
  78. attributes: {
  79. class: false,
  80. x: [ '', null ]
  81. },
  82. children: [ 'foo' ]
  83. } ).render();
  84. expect( el.outerHTML ).to.be.equal( '<p class="false" x="null">foo</p>' );
  85. } );
  86. it( 'renders HTMLElement attributes in a custom namespace', () => {
  87. const el = new Template( {
  88. tag: 'p',
  89. attributes: {
  90. 'class': {
  91. ns: 'foo',
  92. value: [ 'a', 'b' ]
  93. },
  94. x: {
  95. ns: 'abc',
  96. value: 'bar'
  97. }
  98. },
  99. children: [ 'foo' ]
  100. } ).render();
  101. expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
  102. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  103. expect( el.attributes.getNamedItem( 'x' ).namespaceURI ).to.equal( 'abc' );
  104. } );
  105. it( 'creates HTMLElement children', () => {
  106. const el = new Template( {
  107. tag: 'p',
  108. attributes: {
  109. a: 'A'
  110. },
  111. children: [
  112. {
  113. tag: 'b',
  114. children: [ 'B' ]
  115. },
  116. {
  117. tag: 'i',
  118. children: [
  119. 'C',
  120. {
  121. tag: 'b',
  122. children: [ 'D' ]
  123. }
  124. ]
  125. }
  126. ]
  127. } ).render();
  128. expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
  129. } );
  130. it( 'creates a Text node', () => {
  131. const node = new Template( { text: 'foo' } ).render();
  132. expect( node.nodeType ).to.be.equal( 3 );
  133. expect( node.textContent ).to.be.equal( 'foo' );
  134. } );
  135. it( 'creates a child Text Node (different syntaxes)', () => {
  136. const el = new Template( {
  137. tag: 'p',
  138. children: [
  139. 'foo',
  140. { text: 'bar' }
  141. ]
  142. } ).render();
  143. expect( el.outerHTML ).to.be.equal( '<p>foobar</p>' );
  144. } );
  145. it( 'creates multiple child Text Nodes', () => {
  146. const el = new Template( {
  147. tag: 'p',
  148. children: [ 'a', 'b', { text: 'c' }, 'd' ]
  149. } ).render();
  150. expect( el.childNodes ).to.have.length( 4 );
  151. expect( el.outerHTML ).to.be.equal( '<p>abcd</p>' );
  152. } );
  153. it( 'activates listener attachers – root', () => {
  154. const spy1 = testUtils.sinon.spy();
  155. const spy2 = testUtils.sinon.spy();
  156. const spy3 = testUtils.sinon.spy();
  157. const el = new Template( {
  158. tag: 'p',
  159. on: {
  160. _listenerAttachers: {
  161. foo: spy1,
  162. baz: [ spy2, spy3 ]
  163. }
  164. }
  165. } ).render();
  166. sinon.assert.calledWithExactly( spy1, el, 'foo', null );
  167. sinon.assert.calledWithExactly( spy2, el, 'baz', null );
  168. sinon.assert.calledWithExactly( spy3, el, 'baz', null );
  169. } );
  170. it( 'activates listener attachers – children', () => {
  171. const spy = testUtils.sinon.spy();
  172. const el = new Template( {
  173. tag: 'p',
  174. children: [
  175. {
  176. tag: 'span',
  177. on: {
  178. _listenerAttachers: {
  179. bar: spy
  180. }
  181. }
  182. }
  183. ],
  184. } ).render();
  185. sinon.assert.calledWithExactly( spy, el.firstChild, 'bar', null );
  186. } );
  187. it( 'activates listener attachers – DOM selectors', () => {
  188. const spy1 = testUtils.sinon.spy();
  189. const spy2 = testUtils.sinon.spy();
  190. const spy3 = testUtils.sinon.spy();
  191. const spy4 = testUtils.sinon.spy();
  192. const el = new Template( {
  193. tag: 'p',
  194. children: [
  195. {
  196. tag: 'span',
  197. attributes: {
  198. 'id': 'x'
  199. }
  200. },
  201. {
  202. tag: 'span',
  203. attributes: {
  204. 'class': 'y'
  205. },
  206. on: {
  207. _listenerAttachers: {
  208. 'bar@p': spy2
  209. }
  210. }
  211. },
  212. ],
  213. on: {
  214. _listenerAttachers: {
  215. 'foo@span': spy1,
  216. 'baz@.y': [ spy3, spy4 ]
  217. }
  218. }
  219. } ).render();
  220. sinon.assert.calledWithExactly( spy1, el, 'foo', 'span' );
  221. sinon.assert.calledWithExactly( spy2, el.lastChild, 'bar', 'p' );
  222. sinon.assert.calledWithExactly( spy3, el, 'baz', '.y' );
  223. sinon.assert.calledWithExactly( spy4, el, 'baz', '.y' );
  224. } );
  225. it( 'activates model bindings – attributes', () => {
  226. const spy1 = testUtils.sinon.spy();
  227. const spy2 = testUtils.sinon.spy();
  228. const el = new Template( {
  229. tag: 'p',
  230. attributes: {
  231. 'class': {}
  232. },
  233. children: [
  234. {
  235. tag: 'span',
  236. attributes: {
  237. id: {}
  238. },
  239. _modelBinders: {
  240. attributes: {
  241. id: spy2
  242. }
  243. }
  244. }
  245. ],
  246. _modelBinders: {
  247. attributes: {
  248. class: spy1
  249. }
  250. }
  251. } ).render();
  252. sinon.assert.calledWithExactly( spy1, el, sinon.match.object );
  253. sinon.assert.calledWithExactly( spy2, el.firstChild, sinon.match.object );
  254. } );
  255. it( 'activates model bindings – Text Node', () => {
  256. const spy1 = testUtils.sinon.spy();
  257. const spy2 = testUtils.sinon.spy();
  258. const el = new Template( {
  259. tag: 'p',
  260. children: [
  261. {
  262. text: {},
  263. _modelBinders: {
  264. text: spy1
  265. }
  266. },
  267. {
  268. tag: 'span',
  269. children: [
  270. {
  271. text: {},
  272. _modelBinders: {
  273. text: spy2
  274. }
  275. }
  276. ]
  277. }
  278. ]
  279. } ).render();
  280. sinon.assert.calledWithExactly( spy1, el.firstChild, sinon.match.object );
  281. sinon.assert.calledWithExactly( spy2, el.lastChild.firstChild, sinon.match.object );
  282. } );
  283. it( 'uses DOM updater – attributes', () => {
  284. const spy = testUtils.sinon.spy();
  285. const el = new Template( {
  286. tag: 'p',
  287. attributes: {
  288. 'class': {}
  289. },
  290. _modelBinders: {
  291. attributes: {
  292. class: spy
  293. }
  294. }
  295. } ).render();
  296. // Check whether DOM updater is correct.
  297. spy.firstCall.args[ 1 ].set( 'x' );
  298. expect( el.outerHTML ).to.be.equal( '<p class="x"></p>' );
  299. spy.firstCall.args[ 1 ].remove();
  300. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  301. } );
  302. it( 'uses DOM updater – text', () => {
  303. const spy = testUtils.sinon.spy();
  304. const el = new Template( {
  305. tag: 'p',
  306. children: [
  307. {
  308. text: {},
  309. _modelBinders: {
  310. text: spy
  311. }
  312. }
  313. ],
  314. } ).render();
  315. // Check whether DOM updater is correct.
  316. spy.firstCall.args[ 1 ].set( 'x' );
  317. expect( el.outerHTML ).to.be.equal( '<p>x</p>' );
  318. spy.firstCall.args[ 1 ].remove();
  319. expect( el.outerHTML ).to.be.equal( '<p></p>' );
  320. } );
  321. } );
  322. describe( 'apply', () => {
  323. beforeEach( () => {
  324. el = document.createElement( 'div' );
  325. text = document.createTextNode( '' );
  326. } );
  327. it( 'throws when wrong template definition', () => {
  328. expect( () => {
  329. new Template( {
  330. tag: 'p',
  331. text: 'foo'
  332. } ).apply( el );
  333. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  334. } );
  335. it( 'throws when no HTMLElement passed', () => {
  336. expect( () => {
  337. new Template( {
  338. tag: 'p'
  339. } ).apply();
  340. } ).to.throw( CKEditorError, /ui-template-wrong-node/ );
  341. } );
  342. it( 'accepts empty template definition', () => {
  343. new Template( {} ).apply( el );
  344. new Template( {} ).apply( text );
  345. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  346. expect( text.textContent ).to.be.equal( '' );
  347. } );
  348. it( 'applies textContent to a Text Node', () => {
  349. new Template( {
  350. text: 'abc'
  351. } ).apply( text );
  352. expect( text.textContent ).to.be.equal( 'abc' );
  353. } );
  354. it( 'applies attributes to an HTMLElement', () => {
  355. new Template( {
  356. tag: 'div',
  357. attributes: {
  358. 'class': [ 'a', 'b' ],
  359. x: 'bar'
  360. }
  361. } ).apply( el );
  362. expect( el.outerHTML ).to.be.equal( '<div class="a b" x="bar"></div>' );
  363. } );
  364. it( 'applies doesn\'t apply new child to an HTMLElement – Text Node', () => {
  365. new Template( {
  366. tag: 'div',
  367. children: [ 'foo' ]
  368. } ).apply( el );
  369. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  370. } );
  371. it( 'applies doesn\'t apply new child to an HTMLElement – HTMLElement', () => {
  372. new Template( {
  373. tag: 'div',
  374. children: [
  375. {
  376. tag: 'span'
  377. }
  378. ]
  379. } ).apply( el );
  380. expect( el.outerHTML ).to.be.equal( '<div></div>' );
  381. } );
  382. it( 'applies new textContent to an existing Text Node of an HTMLElement', () => {
  383. el.textContent = 'bar';
  384. new Template( {
  385. tag: 'div',
  386. children: [ 'foo' ]
  387. } ).apply( el );
  388. expect( el.outerHTML ).to.be.equal( '<div>foo</div>' );
  389. } );
  390. it( 'applies attributes and TextContent to a DOM tree', () => {
  391. el.textContent = 'abc';
  392. el.appendChild( document.createElement( 'span' ) );
  393. new Template( {
  394. tag: 'div',
  395. attributes: {
  396. 'class': 'parent'
  397. },
  398. children: [
  399. 'Children: ',
  400. {
  401. tag: 'span',
  402. attributes: {
  403. class: 'child'
  404. }
  405. }
  406. ]
  407. } ).apply( el );
  408. expect( el.outerHTML ).to.be.equal( '<div class="parent">Children: <span class="child"></span></div>' );
  409. } );
  410. it( 'activates listener attachers – root', () => {
  411. const spy = testUtils.sinon.spy();
  412. new Template( {
  413. tag: 'div',
  414. on: {
  415. _listenerAttachers: {
  416. click: spy
  417. }
  418. }
  419. } ).apply( el );
  420. sinon.assert.calledWithExactly( spy, el, 'click', null );
  421. } );
  422. it( 'activates listener attachers – children', () => {
  423. const spy = testUtils.sinon.spy();
  424. el.appendChild( document.createElement( 'span' ) );
  425. new Template( {
  426. tag: 'div',
  427. children: [
  428. {
  429. tag: 'span',
  430. on: {
  431. _listenerAttachers: {
  432. click: spy
  433. }
  434. }
  435. }
  436. ]
  437. } ).apply( el );
  438. sinon.assert.calledWithExactly( spy, el.firstChild, 'click', null );
  439. } );
  440. it( 'activates model bindings – root', () => {
  441. const spy = testUtils.sinon.spy();
  442. new Template( {
  443. tag: 'div',
  444. attributes: {
  445. class: {}
  446. },
  447. _modelBinders: {
  448. attributes: {
  449. class: spy
  450. }
  451. }
  452. } ).apply( el );
  453. sinon.assert.calledWithExactly( spy, el, sinon.match.object );
  454. } );
  455. it( 'activates model bindings – children', () => {
  456. const spy = testUtils.sinon.spy();
  457. el.appendChild( document.createElement( 'span' ) );
  458. new Template( {
  459. tag: 'div',
  460. children: [
  461. {
  462. tag: 'span',
  463. attributes: {
  464. class: {}
  465. },
  466. _modelBinders: {
  467. attributes: {
  468. class: spy
  469. }
  470. }
  471. }
  472. ]
  473. } ).apply( el );
  474. sinon.assert.calledWithExactly( spy, el.firstChild, sinon.match.object );
  475. } );
  476. } );
  477. } );