view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document, HTMLElement */
  6. /* bender-tags: ui */
  7. 'use strict';
  8. import testUtils from '/tests/ckeditor5/_utils/utils.js';
  9. import View from '/ckeditor5/ui/view.js';
  10. import Template from '/ckeditor5/ui/template.js';
  11. import Region from '/ckeditor5/ui/region.js';
  12. import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
  13. import Model from '/ckeditor5/ui/model.js';
  14. let TestView, view;
  15. testUtils.createSinonSandbox();
  16. describe( 'View', () => {
  17. describe( 'constructor', () => {
  18. beforeEach( () => {
  19. setTestViewClass();
  20. setTestViewInstance();
  21. } );
  22. it( 'accepts the model', () => {
  23. setTestViewInstance( { a: 'foo', b: 42 } );
  24. expect( view.model ).to.be.an.instanceof( Model );
  25. expect( view ).to.have.deep.property( 'model.a', 'foo' );
  26. expect( view ).to.have.deep.property( 'model.b', 42 );
  27. } );
  28. it( 'defines basic view properties', () => {
  29. const model = new Model();
  30. view = new View( model );
  31. expect( view.model ).to.equal( model );
  32. expect( view.t ).to.be.undefined;
  33. expect( view.regions.length ).to.equal( 0 );
  34. } );
  35. it( 'defines the locale property and the t function', () => {
  36. const model = new Model();
  37. const locale = { t() {} };
  38. view = new View( model, locale );
  39. expect( view.locale ).to.equal( locale );
  40. expect( view.t ).to.equal( locale.t );
  41. } );
  42. } );
  43. describe( 'init', () => {
  44. beforeEach( () => {
  45. setTestViewClass( {
  46. tag: 'p',
  47. children: [
  48. { tag: 'span' },
  49. { tag: 'strong' }
  50. ]
  51. } );
  52. } );
  53. it( 'calls child regions #init', () => {
  54. setTestViewInstance();
  55. const region1 = new Region( 'x' );
  56. const region2 = new Region( 'y' );
  57. view.register( region1, el => el );
  58. view.register( region2, el => el );
  59. const spy1 = testUtils.sinon.spy( region1, 'init' );
  60. const spy2 = testUtils.sinon.spy( region2, 'init' );
  61. view.init();
  62. sinon.assert.calledOnce( spy1 );
  63. sinon.assert.calledOnce( spy2 );
  64. } );
  65. it( 'initializes view regions with string selector', () => {
  66. setTestViewInstance();
  67. const region1 = new Region( 'x' );
  68. const region2 = new Region( 'y' );
  69. view.register( region1, 'span' );
  70. view.register( region2, 'strong' );
  71. view.init();
  72. expect( region1.element ).to.equal( view.element.firstChild );
  73. expect( region2.element ).to.equal( view.element.lastChild );
  74. } );
  75. it( 'initializes view regions with function selector', () => {
  76. setTestViewInstance();
  77. const region1 = new Region( 'x' );
  78. const region2 = new Region( 'y' );
  79. view.register( region1, el => el.firstChild );
  80. view.register( region2, el => el.lastChild );
  81. view.init();
  82. expect( region1.element ).to.equal( view.element.firstChild );
  83. expect( region2.element ).to.equal( view.element.lastChild );
  84. } );
  85. it( 'initializes view regions with boolean selector', () => {
  86. setTestViewInstance();
  87. const region1 = new Region( 'x' );
  88. const region2 = new Region( 'y' );
  89. view.register( region1, true );
  90. view.register( region2, true );
  91. view.init();
  92. expect( region1.element ).to.be.null;
  93. expect( region2.element ).to.be.null;
  94. } );
  95. } );
  96. describe( 'register', () => {
  97. beforeEach( () => {
  98. setTestViewClass();
  99. setTestViewInstance();
  100. } );
  101. it( 'should throw when first argument is neither Region instance nor string', () => {
  102. expect( () => {
  103. view.register( new Date() );
  104. } ).to.throw( CKEditorError, /ui-view-register-wrongtype/ );
  105. } );
  106. it( 'should throw when missing the selector argument', () => {
  107. expect( () => {
  108. view.register( 'x' );
  109. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  110. } );
  111. it( 'should throw when selector argument is of a wrong type', () => {
  112. expect( () => {
  113. view.register( 'x', new Date() );
  114. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  115. expect( () => {
  116. view.register( 'x', false );
  117. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  118. } );
  119. it( 'should throw when overriding an existing region but without override flag set', () => {
  120. expect( () => {
  121. view.register( 'x', true );
  122. view.register( new Region( 'x' ), true );
  123. } ).to.throw( CKEditorError, /ui-view-register-override/ );
  124. } );
  125. it( 'should register a new region with region name as a first argument', () => {
  126. view.register( 'x', true );
  127. expect( view.regions.get( 'x' ) ).to.be.an.instanceof( Region );
  128. } );
  129. it( 'should register a new region with Region instance as a first argument', () => {
  130. view.register( new Region( 'y' ), true );
  131. expect( view.regions.get( 'y' ) ).to.be.an.instanceof( Region );
  132. } );
  133. it( 'should override an existing region with override flag', () => {
  134. view.template = new Template( {
  135. tag: 'div',
  136. children: [
  137. { tag: 'span' }
  138. ]
  139. } );
  140. const region1 = new Region( 'x' );
  141. const region2 = new Region( 'x' );
  142. view.register( region1, true );
  143. view.register( region2, true, true );
  144. view.register( 'x', 'span', true );
  145. view.init();
  146. expect( view.regions.get( 'x' ) ).to.equal( region2 );
  147. expect( view.regions.get( 'x' ).element ).to.equal( view.element.firstChild );
  148. } );
  149. it( 'should not override an existing region with the same region with override flag', () => {
  150. const region = new Region( 'x' );
  151. const spy = testUtils.sinon.spy( view.regions, 'remove' );
  152. view.register( region, true );
  153. view.register( region, true, true );
  154. sinon.assert.notCalled( spy );
  155. } );
  156. } );
  157. describe( 'element', () => {
  158. beforeEach( createViewInstanceWithTemplate );
  159. it( 'invokes out of #template', () => {
  160. setTestViewInstance( { a: 1 } );
  161. expect( view.element ).to.be.an.instanceof( HTMLElement );
  162. expect( view.element.nodeName ).to.equal( 'A' );
  163. } );
  164. it( 'can be explicitly declared', () => {
  165. class CustomView extends View {
  166. constructor() {
  167. super();
  168. this.element = document.createElement( 'span' );
  169. }
  170. }
  171. view = new CustomView();
  172. expect( view.element ).to.be.an.instanceof( HTMLElement );
  173. } );
  174. it( 'is null when there is no template', () => {
  175. expect( new View().element ).to.be.null;
  176. } );
  177. } );
  178. describe( 'destroy', () => {
  179. beforeEach( createViewInstanceWithTemplate );
  180. it( 'should destroy the view', () => {
  181. view.destroy();
  182. expect( view.model ).to.be.null;
  183. expect( view.regions ).to.be.null;
  184. expect( view.template ).to.be.null;
  185. expect( view.locale ).to.be.null;
  186. expect( view.t ).to.be.null;
  187. } );
  188. it( 'detaches the element from DOM', () => {
  189. const elRef = view.element;
  190. document.createElement( 'div' ).appendChild( view.element );
  191. view.destroy();
  192. expect( elRef.parentNode ).to.be.null;
  193. } );
  194. it( 'destroys child regions', () => {
  195. const region = new Region( 'x' );
  196. const spy = testUtils.sinon.spy( region, 'destroy' );
  197. const regionsRef = view.regions;
  198. const regionViewsRef = region.views;
  199. view.register( region, true );
  200. view.regions.get( 'x' ).views.add( new View() );
  201. view.destroy();
  202. expect( regionsRef.length ).to.equal( 0 );
  203. expect( regionViewsRef.length ).to.equal( 0 );
  204. expect( spy.calledOnce ).to.be.true;
  205. } );
  206. it( 'destroy a template–less view', () => {
  207. view = new View();
  208. expect( () => {
  209. view.destroy();
  210. } ).to.not.throw();
  211. } );
  212. } );
  213. describe( 'applyTemplateToElement', () => {
  214. beforeEach( () => {
  215. setTestViewClass();
  216. setTestViewInstance( { a: 'foo', b: 42 } );
  217. } );
  218. it( 'should work for deep DOM structure', () => {
  219. const el = document.createElement( 'div' );
  220. const childA = document.createElement( 'a' );
  221. const childB = document.createElement( 'b' );
  222. childA.textContent = 'anchor';
  223. childB.textContent = 'bold';
  224. el.appendChild( childA );
  225. el.appendChild( childB );
  226. expect( el.outerHTML ).to.equal( '<div><a>anchor</a><b>bold</b></div>' );
  227. const spy1 = testUtils.sinon.spy();
  228. const spy2 = testUtils.sinon.spy();
  229. const spy3 = testUtils.sinon.spy();
  230. const bind = Template.bind( view.model, view );
  231. view.model.on( 'ku', spy1 );
  232. view.model.on( 'kd', spy2 );
  233. view.model.on( 'mo', spy3 );
  234. view.applyTemplateToElement( el, {
  235. tag: 'div',
  236. children: [
  237. {
  238. tag: 'a',
  239. on: {
  240. keyup: bind.to( 'ku' )
  241. },
  242. attributes: {
  243. class: bind.to( 'b', b => 'applied-A-' + b ),
  244. id: 'applied-A'
  245. },
  246. children: [ 'Text applied to childA.' ]
  247. },
  248. {
  249. tag: 'b',
  250. on: {
  251. keydown: bind.to( 'kd' )
  252. },
  253. attributes: {
  254. class: bind.to( 'b', b => 'applied-B-' + b ),
  255. id: 'applied-B'
  256. },
  257. children: [ 'Text applied to childB.' ]
  258. },
  259. 'Text which is not to be applied because it does NOT exist in original element.'
  260. ],
  261. on: {
  262. 'mouseover@a': bind.to( 'mo' )
  263. },
  264. attributes: {
  265. id: bind.to( 'a', a => a.toUpperCase() ),
  266. class: bind.to( 'b', b => 'applied-parent-' + b )
  267. }
  268. } );
  269. expect( el.outerHTML ).to.equal( '<div id="FOO" class="applied-parent-42">' +
  270. '<a class="applied-A-42" id="applied-A">Text applied to childA.</a>' +
  271. '<b class="applied-B-42" id="applied-B">Text applied to childB.</b>' +
  272. '</div>' );
  273. view.model.b = 16;
  274. expect( el.outerHTML ).to.equal( '<div id="FOO" class="applied-parent-16">' +
  275. '<a class="applied-A-16" id="applied-A">Text applied to childA.</a>' +
  276. '<b class="applied-B-16" id="applied-B">Text applied to childB.</b>' +
  277. '</div>' );
  278. document.body.appendChild( el );
  279. // Test "mouseover@a".
  280. dispatchEvent( el, 'mouseover' );
  281. dispatchEvent( childA, 'mouseover' );
  282. // Test "keyup".
  283. dispatchEvent( childA, 'keyup' );
  284. // Test "keydown".
  285. dispatchEvent( childB, 'keydown' );
  286. sinon.assert.calledOnce( spy1 );
  287. sinon.assert.calledOnce( spy2 );
  288. sinon.assert.calledOnce( spy3 );
  289. } );
  290. } );
  291. } );
  292. function createViewInstanceWithTemplate() {
  293. setTestViewClass( { tag: 'a' } );
  294. setTestViewInstance();
  295. }
  296. function setTestViewClass( templateDef, regionsFn ) {
  297. TestView = class V extends View {
  298. constructor( model ) {
  299. super( model );
  300. if ( templateDef ) {
  301. this.template = new Template( templateDef );
  302. }
  303. if ( templateDef && regionsFn ) {
  304. regionsFn.call( this );
  305. }
  306. }
  307. };
  308. }
  309. function setTestViewInstance( model ) {
  310. view = new TestView( new Model( model ) );
  311. if ( view.template ) {
  312. document.body.appendChild( view.element );
  313. }
  314. }
  315. function dispatchEvent( el, domEvtName ) {
  316. if ( !el.parentNode ) {
  317. throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
  318. }
  319. el.dispatchEvent( new Event( domEvtName, {
  320. bubbles: true
  321. } ) );
  322. }