diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js index a00c115dd..d5b70eccf 100644 --- a/src/view/observer/mutationobserver.js +++ b/src/view/observer/mutationobserver.js @@ -12,6 +12,7 @@ import Observer from './observer'; import ViewSelection from '../selection'; import { startsWithFiller, getDataWithoutFiller } from '../filler'; +import isEqualWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqualWith'; /** * Mutation observer class observes changes in the DOM, fires {@link module:engine/view/document~Document#event:mutations} event, mark view @@ -204,16 +205,21 @@ export default class MutationObserver extends Observer { for ( const viewElement of mutatedElements ) { const domElement = domConverter.mapViewToDom( viewElement ); - const viewChildren = viewElement.getChildren(); - const newViewChildren = domConverter.domChildrenToView( domElement ); - - this.renderer.markToSync( 'children', viewElement ); - viewMutations.push( { - type: 'children', - oldChildren: Array.from( viewChildren ), - newChildren: Array.from( newViewChildren ), - node: viewElement - } ); + const viewChildren = Array.from( viewElement.getChildren() ); + const newViewChildren = Array.from( domConverter.domChildrenToView( domElement ) ); + + // It may happen that as a result of many changes (sth was inserted and then removed), + // both elements haven't really changed. #1031 + if ( !isEqualWith( viewChildren, newViewChildren, sameNodes ) ) { + this.renderer.markToSync( 'children', viewElement ); + + viewMutations.push( { + type: 'children', + oldChildren: viewChildren, + newChildren: newViewChildren, + node: viewElement + } ); + } } // Retrieve `domSelection` using `ownerDocument` of one of mutated nodes. @@ -244,6 +250,25 @@ export default class MutationObserver extends Observer { // If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched // view (which has not been changed). In order to "reset DOM" we render the view again. this.document.render(); + + function sameNodes( child1, child2 ) { + // First level of comparison (array of children vs array of children) – use the Lodash's default behavior. + if ( Array.isArray( child1 ) ) { + return; + } + + // Elements. + if ( child1 === child2 ) { + return true; + } + // Texts. + else if ( child1.is( 'text' ) && child2.is( 'text' ) ) { + return child1.data === child2.data; + } + + // Not matching types. + return false; + } } /** diff --git a/tests/view/observer/mutationobserver.js b/tests/view/observer/mutationobserver.js index cec8c271d..18f2a3a97 100644 --- a/tests/view/observer/mutationobserver.js +++ b/tests/view/observer/mutationobserver.js @@ -291,6 +291,40 @@ describe( 'MutationObserver', () => { expect( lastMutations[ 0 ].newText ).to.equal( 'foo ' ); } ); + it( 'should ignore child mutations which resulted in no changes – when element contains elements', () => { + viewRoot.appendChildren( parse( '' ) ); + + viewDocument.render(); + + const domP = domEditor.childNodes[ 2 ]; + const domY = document.createElement( 'y' ); + domP.appendChild( domY ); + domY.remove(); + + mutationObserver.flush(); + + expect( lastMutations.length ).to.equal( 0 ); + } ); + + // This case is more tricky than the previous one because DOMConverter will return a different + // instances of view text nodes every time it converts a DOM text node. + it( 'should ignore child mutations which resulted in no changes – when element contains text nodes', () => { + const domP = domEditor.childNodes[ 0 ]; + const domText = document.createTextNode( 'x' ); + domP.appendChild( domText ); + domText.remove(); + + const domP2 = domEditor.childNodes[ 1 ]; + domP2.appendChild( document.createTextNode( 'x' ) ); + + mutationObserver.flush(); + + // There was onlu P2 change. P1 must be ignored. + const viewP2 = viewRoot.getChild( 1 ); + expect( lastMutations.length ).to.equal( 1 ); + expect( lastMutations[ 0 ].node ).to.equal( viewP2 ); + } ); + it( 'should not ignore mutation with br inserted not on the end of the paragraph', () => { viewRoot.appendChildren( parse( 'foo' ) );