view.js 29 KB

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