mutationobserver.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals document */
  6. import ViewDocument from '../../../src/view/document';
  7. import MutationObserver from '../../../src/view/observer/mutationobserver';
  8. import UIElement from '../../../src/view/uielement';
  9. import createViewRoot from '../_utils/createroot';
  10. import { parse } from '../../../src/dev-utils/view';
  11. describe( 'MutationObserver', () => {
  12. let domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot;
  13. beforeEach( () => {
  14. domRoot = document.createElement( 'div' );
  15. domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
  16. document.body.appendChild( domRoot );
  17. viewDocument = new ViewDocument();
  18. domEditor = document.getElementById( 'main' );
  19. lastMutations = null;
  20. createViewRoot( viewDocument );
  21. viewDocument.attachDomRoot( domEditor );
  22. viewDocument.selection.removeAllRanges();
  23. document.getSelection().removeAllRanges();
  24. mutationObserver = viewDocument.getObserver( MutationObserver );
  25. viewDocument.on( 'mutations', ( evt, mutations ) => {
  26. lastMutations = mutations;
  27. } );
  28. viewRoot = viewDocument.getRoot();
  29. viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
  30. viewDocument.render();
  31. } );
  32. afterEach( () => {
  33. mutationObserver.disable();
  34. domRoot.parentElement.removeChild( domRoot );
  35. viewDocument.destroy();
  36. } );
  37. it( 'should handle typing', () => {
  38. domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
  39. mutationObserver.flush();
  40. expectDomEditorNotToChange();
  41. expect( lastMutations.length ).to.equal( 1 );
  42. expect( lastMutations[ 0 ].type ).to.equal( 'text' );
  43. expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ).getChild( 0 ) );
  44. expect( lastMutations[ 0 ].newText ).to.equal( 'foom' );
  45. expect( lastMutations[ 0 ].oldText ).to.equal( 'foo' );
  46. } );
  47. it( 'should not observe if disabled', () => {
  48. const additional = document.getElementById( 'additional' );
  49. mutationObserver.disable();
  50. createViewRoot( viewDocument, 'div', 'additional' );
  51. viewDocument.attachDomRoot( additional, 'additional' );
  52. additional.textContent = 'foobar';
  53. mutationObserver.flush();
  54. expect( lastMutations ).to.be.null;
  55. } );
  56. it( 'should handle bold', () => {
  57. domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'f';
  58. const domB = document.createElement( 'b' );
  59. domB.appendChild( document.createTextNode( 'oo' ) );
  60. domEditor.childNodes[ 0 ].appendChild( domB );
  61. mutationObserver.flush();
  62. expectDomEditorNotToChange();
  63. expect( lastMutations.length ).to.equal( 1 );
  64. expect( lastMutations[ 0 ].type ).to.equal( 'children' );
  65. expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ) );
  66. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 2 );
  67. expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'f' );
  68. expect( lastMutations[ 0 ].newChildren[ 1 ].name ).to.equal( 'b' );
  69. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 1 );
  70. expect( lastMutations[ 0 ].oldChildren[ 0 ].data ).to.equal( 'foo' );
  71. } );
  72. it( 'should handle unbold', () => {
  73. viewRoot.removeChildren( 0, viewRoot.childCount );
  74. viewRoot.appendChildren( parse( '<container:p><attribute:b>foo</attribute:b></container:p>' ) );
  75. viewDocument.render();
  76. const domP = domEditor.childNodes[ 0 ];
  77. const domB = domP.childNodes[ 0 ];
  78. domP.removeChild( domB );
  79. domP.appendChild( document.createTextNode( 'foo' ) );
  80. mutationObserver.flush();
  81. // "expectDomEditorNotToChange()".
  82. expect( domEditor.childNodes.length ).to.equal( 1 );
  83. expect( domEditor.childNodes[ 0 ].tagName ).to.equal( 'P' );
  84. expect( domEditor.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
  85. expect( domEditor.childNodes[ 0 ].childNodes[ 0 ].tagName ).to.equal( 'B' );
  86. expect( domEditor.childNodes[ 0 ].childNodes[ 0 ].childNodes.length ).to.equal( 1 );
  87. expect( domEditor.childNodes[ 0 ].childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
  88. // Check mutations.
  89. expect( lastMutations.length ).to.equal( 1 );
  90. expect( lastMutations[ 0 ].type ).to.equal( 'children' );
  91. expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ) );
  92. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 1 );
  93. expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'foo' );
  94. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 1 );
  95. expect( lastMutations[ 0 ].oldChildren[ 0 ].name ).to.equal( 'b' );
  96. } );
  97. it( 'should deduplicate text changes', () => {
  98. domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foox';
  99. domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'fooxy';
  100. mutationObserver.flush();
  101. expectDomEditorNotToChange();
  102. expect( lastMutations.length ).to.equal( 1 );
  103. expect( lastMutations[ 0 ].type ).to.equal( 'text' );
  104. expect( lastMutations[ 0 ].node ).to.equal( viewRoot.getChild( 0 ).getChild( 0 ) );
  105. expect( lastMutations[ 0 ].newText ).to.equal( 'fooxy' );
  106. expect( lastMutations[ 0 ].oldText ).to.equal( 'foo' );
  107. } );
  108. it( 'should ignore changes in elements not attached to tree view', () => {
  109. const domP = document.createElement( 'p' );
  110. const domB = document.createElement( 'b' );
  111. const domText = document.createTextNode( 'bom' );
  112. domEditor.appendChild( domP );
  113. domP.appendChild( domB );
  114. domB.appendChild( domText );
  115. mutationObserver.flush();
  116. expectDomEditorNotToChange();
  117. expect( lastMutations.length ).to.equal( 1 );
  118. expect( lastMutations[ 0 ].type ).to.equal( 'children' );
  119. expect( lastMutations[ 0 ].node ).to.equal( viewRoot );
  120. } );
  121. it( 'should fire mutations event with view selection instance, if dom selection can be mapped to view', done => {
  122. const textNode = domEditor.childNodes[ 0 ].childNodes[ 0 ];
  123. textNode.data = 'foom';
  124. const domSelection = document.getSelection();
  125. domSelection.collapse( textNode, 4 );
  126. viewDocument.on( 'mutations', ( evt, viewMutations, viewSelection ) => {
  127. expect( viewSelection.anchor.parent ).to.equal( viewRoot.getChild( 0 ).getChild( 0 ) );
  128. expect( viewSelection.anchor.offset ).to.equal( 4 );
  129. done();
  130. } );
  131. mutationObserver.flush();
  132. expectDomEditorNotToChange();
  133. } );
  134. it( 'should fire mutations event with viewSelection param set to null, if dom selection cannot be mapped to view', done => {
  135. const textNode = domEditor.ownerDocument.createTextNode( 'foo' );
  136. domEditor.childNodes[ 0 ].appendChild( textNode );
  137. const domSelection = document.getSelection();
  138. domSelection.collapse( textNode, 3 );
  139. viewDocument.on( 'mutations', ( evt, viewMutations, viewSelection ) => {
  140. expect( viewSelection ).to.be.null;
  141. done();
  142. } );
  143. mutationObserver.flush();
  144. expectDomEditorNotToChange();
  145. } );
  146. it( 'should be able to observe multiple roots', () => {
  147. const domAdditionalEditor = document.getElementById( 'additional' );
  148. // Prepare AdditionalEditor
  149. createViewRoot( viewDocument, 'div', 'additional' );
  150. viewDocument.attachDomRoot( domAdditionalEditor, 'additional' );
  151. viewDocument.getRoot( 'additional' ).appendChildren(
  152. parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
  153. // Render AdditionalEditor (first editor has been rendered in the beforeEach function)
  154. viewDocument.render();
  155. domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
  156. domAdditionalEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
  157. mutationObserver.flush();
  158. expect( lastMutations.length ).to.equal( 2 );
  159. } );
  160. it( 'should fire nothing if there were no mutations', () => {
  161. mutationObserver.flush();
  162. expectDomEditorNotToChange();
  163. expect( lastMutations ).to.be.null;
  164. } );
  165. it( 'should fire children mutation if the mutation occurred in the inline filler', () => {
  166. const { view, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
  167. viewRoot.appendChildren( view );
  168. viewDocument.selection.setTo( selection );
  169. viewDocument.render();
  170. const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
  171. inlineFiller.data += 'x';
  172. mutationObserver.flush();
  173. expect( lastMutations.length ).to.equal( 1 );
  174. expect( lastMutations[ 0 ].type ).to.equal( 'children' );
  175. expect( lastMutations[ 0 ].node ).to.equal( selection.getFirstPosition().parent );
  176. } );
  177. it( 'should have no inline filler in mutation', () => {
  178. const { view, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
  179. viewRoot.appendChildren( view );
  180. viewDocument.selection.setTo( selection );
  181. viewDocument.render();
  182. let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
  183. inlineFiller.data += 'x';
  184. view.getChild( 1 ).appendChildren( parse( 'x' ) );
  185. mutationObserver.flush();
  186. viewDocument.render();
  187. inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
  188. inlineFiller.data += 'y';
  189. mutationObserver.flush();
  190. expect( lastMutations.length ).to.equal( 1 );
  191. expect( lastMutations[ 0 ].type ).to.equal( 'text' );
  192. expect( lastMutations[ 0 ].oldText ).to.equal( 'x' );
  193. expect( lastMutations[ 0 ].newText ).to.equal( 'xy' );
  194. } );
  195. it( 'should have no block filler in mutation', () => {
  196. viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
  197. viewDocument.render();
  198. const domP = domEditor.childNodes[ 2 ];
  199. domP.removeChild( domP.childNodes[ 0 ] );
  200. domP.appendChild( document.createTextNode( 'foo' ) );
  201. mutationObserver.flush();
  202. expect( lastMutations.length ).to.equal( 1 );
  203. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 1 );
  204. expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'foo' );
  205. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 0 );
  206. } );
  207. it( 'should ignore mutation with bogus br inserted on the end of the empty paragraph', () => {
  208. viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
  209. viewDocument.render();
  210. const domP = domEditor.childNodes[ 2 ];
  211. domP.appendChild( document.createElement( 'br' ) );
  212. mutationObserver.flush();
  213. expect( lastMutations.length ).to.equal( 0 );
  214. } );
  215. it( 'should ignore mutation with bogus br inserted on the end of the paragraph with text', () => {
  216. viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
  217. viewDocument.render();
  218. const domP = domEditor.childNodes[ 2 ];
  219. domP.appendChild( document.createElement( 'br' ) );
  220. mutationObserver.flush();
  221. expect( lastMutations.length ).to.equal( 0 );
  222. } );
  223. it( 'should ignore mutation with bogus br inserted on the end of the paragraph while processing text mutations', () => {
  224. viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
  225. viewDocument.render();
  226. const domP = domEditor.childNodes[ 2 ];
  227. domP.childNodes[ 0 ].data = 'foo ';
  228. domP.appendChild( document.createElement( 'br' ) );
  229. mutationObserver.flush();
  230. expect( lastMutations.length ).to.equal( 1 );
  231. expect( lastMutations[ 0 ].oldText ).to.equal( 'foo' );
  232. expect( lastMutations[ 0 ].newText ).to.equal( 'foo ' );
  233. } );
  234. it( 'should ignore child mutations which resulted in no changes – when element contains elements', () => {
  235. viewRoot.appendChildren( parse( '<container:p><container:x></container:x></container:p>' ) );
  236. viewDocument.render();
  237. const domP = domEditor.childNodes[ 2 ];
  238. const domY = document.createElement( 'y' );
  239. domP.appendChild( domY );
  240. domY.remove();
  241. mutationObserver.flush();
  242. expect( lastMutations.length ).to.equal( 0 );
  243. } );
  244. // This case is more tricky than the previous one because DOMConverter will return a different
  245. // instances of view text nodes every time it converts a DOM text node.
  246. it( 'should ignore child mutations which resulted in no changes – when element contains text nodes', () => {
  247. const domP = domEditor.childNodes[ 0 ];
  248. const domText = document.createTextNode( 'x' );
  249. domP.appendChild( domText );
  250. domText.remove();
  251. const domP2 = domEditor.childNodes[ 1 ];
  252. domP2.appendChild( document.createTextNode( 'x' ) );
  253. mutationObserver.flush();
  254. // There was onlu P2 change. P1 must be ignored.
  255. const viewP2 = viewRoot.getChild( 1 );
  256. expect( lastMutations.length ).to.equal( 1 );
  257. expect( lastMutations[ 0 ].node ).to.equal( viewP2 );
  258. } );
  259. it( 'should not ignore mutation with br inserted not on the end of the paragraph', () => {
  260. viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
  261. viewDocument.render();
  262. const domP = domEditor.childNodes[ 2 ];
  263. domP.insertBefore( document.createElement( 'br' ), domP.childNodes[ 0 ] );
  264. mutationObserver.flush();
  265. expect( lastMutations.length ).to.equal( 1 );
  266. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 2 );
  267. expect( lastMutations[ 0 ].newChildren[ 0 ].name ).to.equal( 'br' );
  268. expect( lastMutations[ 0 ].newChildren[ 1 ].data ).to.equal( 'foo' );
  269. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 1 );
  270. } );
  271. it( 'should not ignore mutation inserting element different than br on the end of the empty paragraph', () => {
  272. viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
  273. viewDocument.render();
  274. const domP = domEditor.childNodes[ 2 ];
  275. domP.appendChild( document.createElement( 'span' ) );
  276. mutationObserver.flush();
  277. expect( lastMutations.length ).to.equal( 1 );
  278. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 1 );
  279. expect( lastMutations[ 0 ].newChildren[ 0 ].name ).to.equal( 'span' );
  280. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 0 );
  281. } );
  282. it( 'should not ignore mutation inserting element different than br on the end of the paragraph with text', () => {
  283. viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
  284. viewDocument.render();
  285. const domP = domEditor.childNodes[ 2 ];
  286. domP.appendChild( document.createElement( 'span' ) );
  287. mutationObserver.flush();
  288. expect( lastMutations.length ).to.equal( 1 );
  289. expect( lastMutations[ 0 ].newChildren.length ).to.equal( 2 );
  290. expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'foo' );
  291. expect( lastMutations[ 0 ].newChildren[ 1 ].name ).to.equal( 'span' );
  292. expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 1 );
  293. } );
  294. describe( 'UIElement integration', () => {
  295. function createUIElement( name ) {
  296. const element = new UIElement( name );
  297. element.render = function( domDocument ) {
  298. const root = this.toDomElement( domDocument );
  299. root.innerHTML = 'foo bar';
  300. return root;
  301. };
  302. return element;
  303. }
  304. beforeEach( () => {
  305. const uiElement = createUIElement( 'div' );
  306. viewRoot.appendChildren( uiElement );
  307. viewDocument.render();
  308. } );
  309. it( 'should not collect text mutations from UIElement', () => {
  310. domEditor.childNodes[ 2 ].childNodes[ 0 ].data = 'foom';
  311. mutationObserver.flush();
  312. expect( lastMutations.length ).to.equal( 0 );
  313. } );
  314. it( 'should not collect child mutations from UIElement', () => {
  315. const span = document.createElement( 'span' );
  316. domEditor.childNodes[ 2 ].appendChild( span );
  317. mutationObserver.flush();
  318. expect( lastMutations.length ).to.equal( 0 );
  319. } );
  320. } );
  321. function expectDomEditorNotToChange() {
  322. expect( domEditor.childNodes.length ).to.equal( 2 );
  323. expect( domEditor.childNodes[ 0 ].tagName ).to.equal( 'P' );
  324. expect( domEditor.childNodes[ 1 ].tagName ).to.equal( 'P' );
  325. expect( domEditor.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
  326. expect( domEditor.childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
  327. expect( domEditor.childNodes[ 1 ].childNodes.length ).to.equal( 1 );
  328. expect( domEditor.childNodes[ 1 ].childNodes[ 0 ].data ).to.equal( 'bar' );
  329. }
  330. } );