view.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document, console */
  6. import View from '../../../src/view/view';
  7. import Observer from '../../../src/view/observer/observer';
  8. import MutationObserver from '../../../src/view/observer/mutationobserver';
  9. import KeyObserver from '../../../src/view/observer/keyobserver';
  10. import InputObserver from '../../../src/view/observer/inputobserver';
  11. import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
  12. import SelectionObserver from '../../../src/view/observer/selectionobserver';
  13. import FocusObserver from '../../../src/view/observer/focusobserver';
  14. import CompositionObserver from '../../../src/view/observer/compositionobserver';
  15. import ViewRange from '../../../src/view/range';
  16. import ViewElement from '../../../src/view/element';
  17. import ViewPosition from '../../../src/view/position';
  18. import ViewSelection from '../../../src/view/selection';
  19. import count from '@ckeditor/ckeditor5-utils/src/count';
  20. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  21. import createViewRoot from '../_utils/createroot';
  22. import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
  23. import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  24. import env from '@ckeditor/ckeditor5-utils/src/env';
  25. describe( 'view', () => {
  26. const DEFAULT_OBSERVERS_COUNT = 6;
  27. let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
  28. beforeEach( () => {
  29. domRoot = createElement( document, 'div', {
  30. id: 'editor',
  31. contenteditable: 'true'
  32. } );
  33. document.body.appendChild( domRoot );
  34. view = new View();
  35. viewDocument = view.document;
  36. ObserverMock = class extends Observer {
  37. constructor( view ) {
  38. super( view );
  39. this.enable = sinon.spy();
  40. this.disable = sinon.spy();
  41. this.observe = sinon.spy();
  42. this.destroy = sinon.spy();
  43. }
  44. };
  45. instantiated = 0;
  46. enabled = 0;
  47. ObserverMockGlobalCount = class extends Observer {
  48. constructor( view ) {
  49. super( view );
  50. instantiated++;
  51. this.observe = sinon.spy();
  52. }
  53. enable() {
  54. enabled++;
  55. }
  56. };
  57. } );
  58. afterEach( () => {
  59. sinon.restore();
  60. domRoot.remove();
  61. view.destroy();
  62. } );
  63. it( 'should add default observers', () => {
  64. expect( count( view._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
  65. expect( view.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
  66. expect( view.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
  67. expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
  68. expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
  69. expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
  70. expect( view.getObserver( CompositionObserver ) ).to.be.instanceof( CompositionObserver );
  71. } );
  72. it( 'should add InputObserver on Android devices', () => {
  73. const oldEnvIsAndroid = env.isAndroid;
  74. env.isAndroid = true;
  75. const newView = new View();
  76. expect( newView.getObserver( InputObserver ) ).to.be.instanceof( InputObserver );
  77. env.isAndroid = oldEnvIsAndroid;
  78. newView.destroy();
  79. } );
  80. describe( 'attachDomRoot()', () => {
  81. it( 'should attach DOM element to main view element', () => {
  82. const domDiv = document.createElement( 'div' );
  83. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  84. expect( count( view.domRoots ) ).to.equal( 0 );
  85. view.attachDomRoot( domDiv );
  86. expect( count( view.domRoots ) ).to.equal( 1 );
  87. expect( view.getDomRoot() ).to.equal( domDiv );
  88. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
  89. expect( view._renderer.markedChildren.has( viewRoot ) ).to.be.true;
  90. domDiv.remove();
  91. } );
  92. it( 'should attach DOM element to custom view element', () => {
  93. const domH1 = document.createElement( 'h1' );
  94. const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
  95. expect( count( view.domRoots ) ).to.equal( 0 );
  96. view.attachDomRoot( domH1, 'header' );
  97. expect( count( view.domRoots ) ).to.equal( 1 );
  98. expect( view.getDomRoot( 'header' ) ).to.equal( domH1 );
  99. expect( view.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
  100. expect( view._renderer.markedChildren.has( viewH1 ) ).to.be.true;
  101. } );
  102. it( 'should handle the "contenteditable" attribute management on #isReadOnly change', () => {
  103. const domDiv = document.createElement( 'div' );
  104. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  105. view.attachDomRoot( domDiv );
  106. viewRoot.isReadOnly = false;
  107. expect( viewRoot.getAttribute( 'contenteditable' ) ).to.equal( 'true' );
  108. viewRoot.isReadOnly = true;
  109. expect( viewRoot.getAttribute( 'contenteditable' ) ).to.equal( 'false' );
  110. } );
  111. it( 'should handle the ".ck-read-only" class management on #isReadOnly change', () => {
  112. const domDiv = document.createElement( 'div' );
  113. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  114. view.attachDomRoot( domDiv );
  115. viewRoot.isReadOnly = false;
  116. expect( viewRoot.hasClass( 'ck-read-only' ) ).to.be.false;
  117. viewRoot.isReadOnly = true;
  118. expect( viewRoot.hasClass( 'ck-read-only' ) ).to.be.true;
  119. } );
  120. it( 'should call observe on each observer', () => {
  121. // The variable will be overwritten.
  122. view.destroy();
  123. view = new View();
  124. viewDocument = view.document;
  125. view._renderer.render = sinon.spy();
  126. const domDiv1 = document.createElement( 'div' );
  127. domDiv1.setAttribute( 'id', 'editor' );
  128. const domDiv2 = document.createElement( 'div' );
  129. domDiv2.setAttribute( 'id', 'editor' );
  130. const observerMock = view.addObserver( ObserverMock );
  131. const observerMockGlobalCount = view.addObserver( ObserverMockGlobalCount );
  132. createViewRoot( viewDocument, 'div', 'root1' );
  133. view.attachDomRoot( document.createElement( 'div' ), 'root1' );
  134. sinon.assert.calledOnce( observerMock.observe );
  135. sinon.assert.calledOnce( observerMockGlobalCount.observe );
  136. } );
  137. } );
  138. describe( 'detachDomRoot()', () => {
  139. it( 'should remove DOM root and unbind DOM element', () => {
  140. const domDiv = document.createElement( 'div' );
  141. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  142. view.attachDomRoot( domDiv );
  143. expect( count( view.domRoots ) ).to.equal( 1 );
  144. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
  145. view.detachDomRoot( 'main' );
  146. expect( count( view.domRoots ) ).to.equal( 0 );
  147. expect( view.domConverter.mapViewToDom( viewRoot ) ).to.be.undefined;
  148. domDiv.remove();
  149. } );
  150. it( 'should restore the DOM root attributes to the state before attachDomRoot()', () => {
  151. const domDiv = document.createElement( 'div' );
  152. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  153. domDiv.setAttribute( 'foo', 'bar' );
  154. domDiv.setAttribute( 'data-baz', 'qux' );
  155. domDiv.classList.add( 'foo-class' );
  156. view.attachDomRoot( domDiv );
  157. view.change( writer => {
  158. writer.addClass( 'addedClass', viewRoot );
  159. writer.setAttribute( 'added-attribute', 'foo', viewRoot );
  160. writer.setAttribute( 'foo', 'changed the value', viewRoot );
  161. } );
  162. view.detachDomRoot( 'main' );
  163. const attributes = {};
  164. for ( const attribute of Array.from( domDiv.attributes ) ) {
  165. attributes[ attribute.name ] = attribute.value;
  166. }
  167. expect( attributes ).to.deep.equal( {
  168. foo: 'bar',
  169. 'data-baz': 'qux',
  170. class: 'foo-class'
  171. } );
  172. domDiv.remove();
  173. } );
  174. it( 'should remove the "contenteditable" attribute from the DOM root', () => {
  175. const domDiv = document.createElement( 'div' );
  176. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  177. view.attachDomRoot( domDiv );
  178. view.forceRender();
  179. viewRoot.isReadOnly = false;
  180. expect( domDiv.getAttribute( 'contenteditable' ) ).to.equal( 'true' );
  181. view.detachDomRoot( 'main' );
  182. expect( domDiv.hasAttribute( 'contenteditable' ) ).to.be.false;
  183. domDiv.remove();
  184. } );
  185. it( 'should remove the ".ck-read-only" class from the DOM root', () => {
  186. const domDiv = document.createElement( 'div' );
  187. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  188. view.attachDomRoot( domDiv );
  189. view.forceRender();
  190. viewRoot.isReadOnly = true;
  191. expect( domDiv.classList.contains( 'ck-read-only' ) ).to.be.true;
  192. view.detachDomRoot( 'main' );
  193. expect( domDiv.classList.contains( 'ck-read-only' ) ).to.be.false;
  194. domDiv.remove();
  195. } );
  196. } );
  197. describe( 'addObserver()', () => {
  198. beforeEach( () => {
  199. // The variable will be overwritten.
  200. view.destroy();
  201. view = new View();
  202. viewDocument = view.document;
  203. view._renderer.render = sinon.spy();
  204. } );
  205. afterEach( () => {
  206. view.destroy();
  207. } );
  208. it( 'should be instantiated and enabled on adding', () => {
  209. const observerMock = view.addObserver( ObserverMock );
  210. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
  211. expect( observerMock ).to.have.property( 'document', viewDocument );
  212. sinon.assert.calledOnce( observerMock.enable );
  213. } );
  214. it( 'should return observer instance each time addObserver is called', () => {
  215. const observerMock1 = view.addObserver( ObserverMock );
  216. const observerMock2 = view.addObserver( ObserverMock );
  217. expect( observerMock1 ).to.be.instanceof( ObserverMock );
  218. expect( observerMock2 ).to.be.instanceof( ObserverMock );
  219. expect( observerMock1 ).to.equals( observerMock2 );
  220. } );
  221. it( 'should instantiate one observer only once', () => {
  222. view.addObserver( ObserverMockGlobalCount );
  223. view.addObserver( ObserverMockGlobalCount );
  224. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
  225. expect( instantiated ).to.equal( 1 );
  226. expect( enabled ).to.equal( 1 );
  227. view.addObserver( ObserverMock );
  228. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
  229. } );
  230. it( 'should instantiate child class of already registered observer', () => {
  231. class ObserverMock extends Observer {
  232. enable() {}
  233. }
  234. class ChildObserverMock extends ObserverMock {
  235. enable() {}
  236. }
  237. view.addObserver( ObserverMock );
  238. view.addObserver( ChildObserverMock );
  239. expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
  240. } );
  241. it( 'should be disabled and re-enabled on render', () => {
  242. const observerMock = view.addObserver( ObserverMock );
  243. view.forceRender();
  244. sinon.assert.calledOnce( observerMock.disable );
  245. sinon.assert.calledOnce( view._renderer.render );
  246. sinon.assert.calledTwice( observerMock.enable );
  247. } );
  248. it( 'should call observe on each root', () => {
  249. createViewRoot( viewDocument, 'div', 'roo1' );
  250. createViewRoot( viewDocument, 'div', 'roo2' );
  251. view.attachDomRoot( document.createElement( 'div' ), 'roo1' );
  252. view.attachDomRoot( document.createElement( 'div' ), 'roo2' );
  253. const observerMock = view.addObserver( ObserverMock );
  254. sinon.assert.calledTwice( observerMock.observe );
  255. } );
  256. } );
  257. describe( 'getObserver()', () => {
  258. it( 'should return observer it it is added', () => {
  259. const addedObserverMock = view.addObserver( ObserverMock );
  260. const getObserverMock = view.getObserver( ObserverMock );
  261. expect( getObserverMock ).to.be.instanceof( ObserverMock );
  262. expect( getObserverMock ).to.equal( addedObserverMock );
  263. } );
  264. it( 'should return undefined if observer is not added', () => {
  265. const getObserverMock = view.getObserver( ObserverMock );
  266. expect( getObserverMock ).to.be.undefined;
  267. } );
  268. } );
  269. describe( 'scrollToTheSelection()', () => {
  270. beforeEach( () => {
  271. // Silence the Rect warnings.
  272. sinon.stub( console, 'warn' );
  273. } );
  274. it( 'does nothing when there are no ranges in the selection', () => {
  275. const stub = sinon.stub( global.window, 'scrollTo' );
  276. view.scrollToTheSelection();
  277. sinon.assert.notCalled( stub );
  278. } );
  279. it( 'scrolls to the first range in selection with an offset', () => {
  280. const root = createViewRoot( viewDocument, 'div', 'main' );
  281. const stub = sinon.stub( global.window, 'scrollTo' );
  282. const range = ViewRange._createIn( root );
  283. view.attachDomRoot( domRoot );
  284. view.change( writer => {
  285. writer.setSelection( range );
  286. } );
  287. // Make sure the window will have to scroll to the domRoot.
  288. Object.assign( domRoot.style, {
  289. position: 'absolute',
  290. top: '-1000px',
  291. left: '-1000px'
  292. } );
  293. view.scrollToTheSelection();
  294. sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
  295. } );
  296. } );
  297. describe( 'disableObservers()', () => {
  298. it( 'should disable observers', () => {
  299. const addedObserverMock = view.addObserver( ObserverMock );
  300. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  301. expect( addedObserverMock.disable.called ).to.be.false;
  302. view.disableObservers();
  303. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  304. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  305. } );
  306. } );
  307. describe( 'enableObservers()', () => {
  308. it( 'should enable observers', () => {
  309. const addedObserverMock = view.addObserver( ObserverMock );
  310. view.disableObservers();
  311. expect( addedObserverMock.enable.calledOnce ).to.be.true;
  312. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  313. view.enableObservers();
  314. expect( addedObserverMock.enable.calledTwice ).to.be.true;
  315. expect( addedObserverMock.disable.calledOnce ).to.be.true;
  316. } );
  317. } );
  318. describe( 'focus()', () => {
  319. let domEditable, viewEditable;
  320. beforeEach( () => {
  321. domEditable = document.createElement( 'div' );
  322. domEditable.setAttribute( 'contenteditable', 'true' );
  323. document.body.appendChild( domEditable );
  324. viewEditable = createViewRoot( viewDocument, 'div', 'main' );
  325. view.attachDomRoot( domEditable );
  326. view.change( writer => {
  327. writer.setSelection( viewEditable, 0 );
  328. } );
  329. } );
  330. afterEach( () => {
  331. document.body.removeChild( domEditable );
  332. } );
  333. it( 'should focus editable with selection', () => {
  334. const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
  335. const renderSpy = sinon.spy( view, 'forceRender' );
  336. view.focus();
  337. expect( converterFocusSpy.called ).to.be.true;
  338. expect( renderSpy.calledOnce ).to.be.true;
  339. expect( document.activeElement ).to.equal( domEditable );
  340. const domSelection = document.getSelection();
  341. expect( domSelection.rangeCount ).to.equal( 1 );
  342. const domRange = domSelection.getRangeAt( 0 );
  343. expect( domRange.startContainer ).to.equal( domEditable );
  344. expect( domRange.startOffset ).to.equal( 0 );
  345. expect( domRange.collapsed ).to.be.true;
  346. } );
  347. it( 'should not focus if document is already focused', () => {
  348. const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
  349. const renderSpy = sinon.spy( view, 'forceRender' );
  350. viewDocument.isFocused = true;
  351. view.focus();
  352. expect( converterFocusSpy.called ).to.be.false;
  353. expect( renderSpy.called ).to.be.false;
  354. } );
  355. it( 'should not crash when there is no selection', () => {
  356. // Catches the `There is no selection in any editable to focus.` warning in the CK_DEBUG mode.
  357. sinon.stub( console, 'warn' );
  358. view.change( writer => {
  359. writer.setSelection( null );
  360. } );
  361. view.focus();
  362. } );
  363. } );
  364. describe( 'isFocused', () => {
  365. it( 'should change renderer.isFocused too', () => {
  366. expect( viewDocument.isFocused ).to.equal( false );
  367. expect( view._renderer.isFocused ).to.equal( false );
  368. viewDocument.isFocused = true;
  369. expect( viewDocument.isFocused ).to.equal( true );
  370. expect( view._renderer.isFocused ).to.equal( true );
  371. } );
  372. } );
  373. describe( 'isRenderingInProgress', () => {
  374. it( 'should be true while rendering is in progress', () => {
  375. expect( view.isRenderingInProgress ).to.equal( false );
  376. const spy = sinon.spy();
  377. view.on( 'change:isRenderingInProgress', spy );
  378. view.fire( 'render' );
  379. sinon.assert.calledTwice( spy );
  380. sinon.assert.calledWith( spy.firstCall, sinon.match.any, 'isRenderingInProgress', true );
  381. sinon.assert.calledWith( spy.secondCall, sinon.match.any, 'isRenderingInProgress', false );
  382. } );
  383. } );
  384. describe( 'forceRender()', () => {
  385. it( 'disable observers, renders and enable observers', () => {
  386. const observerMock = view.addObserver( ObserverMock );
  387. const renderStub = sinon.stub( view._renderer, 'render' );
  388. view.forceRender();
  389. sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
  390. } );
  391. it( 'should fire `render` and `layoutChanged` even if there were no changes', () => {
  392. const renderSpy = sinon.spy();
  393. const layoutChangedSpy = sinon.spy();
  394. view.on( 'render', renderSpy );
  395. view.document.on( 'layoutChanged', layoutChangedSpy );
  396. view.forceRender();
  397. sinon.assert.calledOnce( renderSpy );
  398. sinon.assert.calledOnce( layoutChangedSpy );
  399. } );
  400. it( 'should fire `render` and `layoutChanged` if there is some buffered change', () => {
  401. const renderSpy = sinon.spy();
  402. const layoutChangedSpy = sinon.spy();
  403. view.on( 'render', renderSpy );
  404. view.document.on( 'layoutChanged', layoutChangedSpy );
  405. view.document.selection._setTo( null );
  406. view.forceRender();
  407. sinon.assert.calledOnce( renderSpy );
  408. sinon.assert.calledOnce( layoutChangedSpy );
  409. } );
  410. } );
  411. describe( 'view and DOM integration', () => {
  412. it( 'should remove content of the DOM', () => {
  413. const domDiv = createElement( document, 'div', { id: 'editor' }, [
  414. createElement( document, 'p' ),
  415. createElement( document, 'p' )
  416. ] );
  417. const view = new View();
  418. const viewDocument = view.document;
  419. createViewRoot( viewDocument, 'div', 'main' );
  420. view.attachDomRoot( domDiv );
  421. view.forceRender();
  422. expect( domDiv.childNodes.length ).to.equal( 1 );
  423. expect( view.domConverter.isBlockFiller( domDiv.childNodes[ 0 ] ) ).to.be.true;
  424. view.destroy();
  425. domDiv.remove();
  426. } );
  427. it( 'should render changes in the Document', () => {
  428. const domDiv = document.createElement( 'div' );
  429. const view = new View();
  430. const viewDocument = view.document;
  431. createViewRoot( viewDocument, 'div', 'main' );
  432. view.attachDomRoot( domDiv );
  433. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  434. view.forceRender();
  435. expect( domDiv.childNodes.length ).to.equal( 1 );
  436. expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
  437. view.destroy();
  438. } );
  439. it( 'should render attribute changes', () => {
  440. const domRoot = document.createElement( 'div' );
  441. const view = new View();
  442. const viewDocument = view.document;
  443. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  444. view.attachDomRoot( domRoot );
  445. const viewP = new ViewElement( 'p', { class: 'foo' } );
  446. viewRoot._appendChild( viewP );
  447. view.forceRender();
  448. expect( domRoot.childNodes.length ).to.equal( 1 );
  449. expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
  450. viewP._setAttribute( 'class', 'bar' );
  451. view.forceRender();
  452. expect( domRoot.childNodes.length ).to.equal( 1 );
  453. expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
  454. view.destroy();
  455. domRoot.remove();
  456. } );
  457. } );
  458. describe( 'change()', () => {
  459. it( 'should throw when someone tries to change view during rendering', () => {
  460. const domDiv = document.createElement( 'div' );
  461. const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
  462. let renderingCalled = false;
  463. view.attachDomRoot( domDiv );
  464. view.change( writer => {
  465. const p = writer.createContainerElement( 'p' );
  466. const ui = writer.createUIElement( 'span', null, function( domDocument ) {
  467. const element = this.toDomElement( domDocument );
  468. expectToThrowCKEditorError( () => {
  469. view.change( () => {} );
  470. }, /^cannot-change-view-tree/, view );
  471. renderingCalled = true;
  472. return element;
  473. } );
  474. writer.insert( ViewPosition._createAt( p, 0 ), ui );
  475. writer.insert( ViewPosition._createAt( viewRoot, 0 ), p );
  476. } );
  477. expect( renderingCalled ).to.be.true;
  478. domDiv.remove();
  479. } );
  480. it( 'should throw when someone tries to use change() method in post-fixer', () => {
  481. const domDiv = document.createElement( 'div' );
  482. createViewRoot( viewDocument, 'div', 'main' );
  483. view.attachDomRoot( domDiv );
  484. viewDocument.registerPostFixer( () => {
  485. expectToThrowCKEditorError( () => {
  486. view.change( () => {} );
  487. }, /^cannot-change-view-tree/, view );
  488. } );
  489. view.forceRender();
  490. domDiv.remove();
  491. } );
  492. it( 'should fire render event and it should trigger rendering before listeners on normal priority', () => {
  493. const renderSpy = sinon.spy( view._renderer, 'render' );
  494. const eventSpy = sinon.spy();
  495. view.on( 'render', eventSpy );
  496. view.change( writer => {
  497. writer.setSelection( null );
  498. } );
  499. sinon.assert.callOrder( renderSpy, eventSpy );
  500. } );
  501. it( 'should fire render event once for nested change blocks', () => {
  502. const renderSpy = sinon.spy( view._renderer, 'render' );
  503. const eventSpy = sinon.spy();
  504. const viewEditable = createViewRoot( viewDocument, 'div', 'main' );
  505. view.on( 'render', eventSpy );
  506. view.change( writer => {
  507. writer.setSelection( null );
  508. view.change( writer => {
  509. writer.setSelection( viewEditable, 0 );
  510. } );
  511. view.change( writer => {
  512. writer.setSelection( null );
  513. view.change( writer => {
  514. writer.setSelection( viewEditable, 0 );
  515. } );
  516. } );
  517. view.change( writer => {
  518. writer.setSelection( null );
  519. } );
  520. } );
  521. sinon.assert.calledOnce( renderSpy );
  522. sinon.assert.calledOnce( eventSpy );
  523. sinon.assert.callOrder( renderSpy, eventSpy );
  524. } );
  525. it( 'should fire render event once even if render is called during the change', () => {
  526. const renderSpy = sinon.spy( view._renderer, 'render' );
  527. const eventSpy = sinon.spy();
  528. view.on( 'render', eventSpy );
  529. view.change( () => {
  530. view.forceRender();
  531. view.change( writer => {
  532. writer.setSelection( null );
  533. view.forceRender();
  534. } );
  535. view.forceRender();
  536. } );
  537. sinon.assert.calledOnce( renderSpy );
  538. sinon.assert.calledOnce( eventSpy );
  539. sinon.assert.callOrder( renderSpy, eventSpy );
  540. } );
  541. it( 'should call post-fixers after change but before rendering', () => {
  542. const postFixer1 = sinon.spy( () => false );
  543. const postFixer2 = sinon.spy( () => false );
  544. const changeSpy = sinon.spy();
  545. const eventSpy = sinon.spy();
  546. viewDocument.registerPostFixer( postFixer1 );
  547. viewDocument.registerPostFixer( postFixer2 );
  548. view.on( 'render', eventSpy );
  549. view.change( writer => {
  550. changeSpy();
  551. writer.setSelection( null );
  552. } );
  553. sinon.assert.calledOnce( postFixer1 );
  554. sinon.assert.calledOnce( postFixer2 );
  555. sinon.assert.calledOnce( changeSpy );
  556. sinon.assert.calledOnce( eventSpy );
  557. sinon.assert.callOrder( changeSpy, postFixer1, postFixer2, eventSpy );
  558. } );
  559. it( 'should call post-fixers until all are done', () => {
  560. let called = false;
  561. const postFixer1 = sinon.spy();
  562. const postFixer2 = sinon.spy();
  563. const changeSpy = sinon.spy();
  564. const eventSpy = sinon.spy();
  565. viewDocument.registerPostFixer( () => {
  566. if ( !called ) {
  567. called = true;
  568. postFixer1();
  569. return true;
  570. }
  571. postFixer2();
  572. return false;
  573. } );
  574. view.on( 'render', eventSpy );
  575. view.change( writer => {
  576. changeSpy();
  577. writer.setSelection( null );
  578. } );
  579. sinon.assert.calledOnce( postFixer1 );
  580. sinon.assert.calledOnce( postFixer2 );
  581. sinon.assert.calledOnce( changeSpy );
  582. sinon.assert.calledOnce( eventSpy );
  583. sinon.assert.callOrder( changeSpy, postFixer1, postFixer2, eventSpy );
  584. } );
  585. it( 'should return result of the callback', () => {
  586. const result = view.change( () => {
  587. return 'FooBar';
  588. } );
  589. expect( result ).to.equal( 'FooBar' );
  590. } );
  591. it( 'should return result of the callback with nested change block', () => {
  592. let result2 = false;
  593. let result3 = false;
  594. const result1 = view.change( () => {
  595. return view.change( () => {
  596. result2 = view.change( () => {
  597. return true;
  598. } );
  599. result3 = view.change( () => {} );
  600. return 42;
  601. } );
  602. } );
  603. expect( result1 ).to.equal( 42 );
  604. expect( result2 ).to.equal( true );
  605. expect( result3 ).to.undefined;
  606. } );
  607. } );
  608. describe( 'createPositionAt()', () => {
  609. it( 'should return instance of Position', () => {
  610. createViewRoot( viewDocument, 'div', 'main' );
  611. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  612. expect( view.createPositionAt( viewDocument.getRoot(), 0 ) ).to.be.instanceof( ViewPosition );
  613. } );
  614. } );
  615. describe( 'createPositionAfter()', () => {
  616. it( 'should return instance of Position', () => {
  617. createViewRoot( viewDocument, 'div', 'main' );
  618. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  619. expect( view.createPositionAfter( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewPosition );
  620. } );
  621. } );
  622. describe( 'createPositionBefore()', () => {
  623. it( 'should return instance of Position', () => {
  624. createViewRoot( viewDocument, 'div', 'main' );
  625. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  626. expect( view.createPositionBefore( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewPosition );
  627. } );
  628. } );
  629. describe( 'createRange()', () => {
  630. it( 'should return instance of Range', () => {
  631. createViewRoot( viewDocument, 'div', 'main' );
  632. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  633. expect( view.createRange( view.createPositionAt( viewDocument.getRoot(), 0 ) ) ).to.be.instanceof( ViewRange );
  634. } );
  635. } );
  636. describe( 'createRangeIn()', () => {
  637. it( 'should return instance of Range', () => {
  638. createViewRoot( viewDocument, 'div', 'main' );
  639. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  640. expect( view.createRangeIn( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewRange );
  641. } );
  642. } );
  643. describe( 'createRangeOn()', () => {
  644. it( 'should return instance of Range', () => {
  645. createViewRoot( viewDocument, 'div', 'main' );
  646. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  647. expect( view.createRangeOn( viewDocument.getRoot().getChild( 0 ) ) ).to.be.instanceof( ViewRange );
  648. } );
  649. } );
  650. describe( 'createSelection()', () => {
  651. it( 'should return instance of Selection', () => {
  652. createViewRoot( viewDocument, 'div', 'main' );
  653. viewDocument.getRoot()._appendChild( new ViewElement( 'p' ) );
  654. expect( view.createSelection() ).to.be.instanceof( ViewSelection );
  655. } );
  656. } );
  657. describe( 'destroy()', () => {
  658. it( 'should destroy all observers', () => {
  659. const observerMock = view.addObserver( ObserverMock );
  660. view.destroy();
  661. sinon.assert.calledOnce( observerMock.destroy );
  662. } );
  663. } );
  664. } );