template.js 9.2 KB

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