Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 88fcdcb

Browse files
authored
Merge pull request #989 from ckeditor/t/922
Fix: Prevent unbinding elements that are reused during rendering. Closes #922. BREAKING CHANGE: Removed `Renderer#getCorrespondingDom()` and `Renderer#getCorrespondingView()` methods. BREAKING CHANGE: Renamed `Renderer#getCorrespondingDomText()` method to `Renderer#findCorrespondingDomText()` and `Renderer#getCorrespondingViewText()` to `Renderer#findCorrespondingViewText()`. BREAKING CHANGE: Merged `Renderer#getCorrespondingDomElement()` and `Renderer#getCorrespondingDomDocumentFragment()` into one method `Renderer#mapViewToDom()`. BREAKING CHANGE: Merged `Renderer#getCorrespondingViewElement()` and `Renderer#getCorrespondingViewDocumentFragment()` into `Renderer#mapDomToView()`.
2 parents 4c9a0af + 2dcc99d commit 88fcdcb

File tree

11 files changed

+329
-297
lines changed

11 files changed

+329
-297
lines changed

src/view/domconverter.js

Lines changed: 63 additions & 118 deletions
Large diffs are not rendered by default.

src/view/observer/domeventdata.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default class DomEventData {
5555
* @type module:engine/view/element~Element
5656
*/
5757
get target() {
58-
return this.document.domConverter.getCorrespondingViewElement( this.domTarget );
58+
return this.document.domConverter.mapDomToView( this.domTarget );
5959
}
6060

6161
/**

src/view/observer/mutationobserver.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export default class MutationObserver extends Observer {
147147
// element with changed structure anyway.
148148
for ( const mutation of domMutations ) {
149149
if ( mutation.type === 'childList' ) {
150-
const element = domConverter.getCorrespondingViewElement( mutation.target );
150+
const element = domConverter.mapDomToView( mutation.target );
151151

152152
// Do not collect mutations from UIElements.
153153
if ( element && element.is( 'uiElement' ) ) {
@@ -162,15 +162,15 @@ export default class MutationObserver extends Observer {
162162

163163
// Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
164164
for ( const mutation of domMutations ) {
165-
const element = domConverter.getCorrespondingViewElement( mutation.target );
165+
const element = domConverter.mapDomToView( mutation.target );
166166

167167
// Do not collect mutations from UIElements.
168168
if ( element && element.is( 'uiElement' ) ) {
169169
continue;
170170
}
171171

172172
if ( mutation.type === 'characterData' ) {
173-
const text = domConverter.getCorrespondingViewText( mutation.target );
173+
const text = domConverter.findCorrespondingViewText( mutation.target );
174174

175175
if ( text && !mutatedElements.has( text.parent ) ) {
176176
// Use text as a key, for deduplication. If there will be another mutation on the same text element
@@ -186,7 +186,7 @@ export default class MutationObserver extends Observer {
186186
// on text, but for the view, where filler text node did not existed, new text node was created, so we
187187
// need to fire 'children' mutation instead of 'text'.
188188
else if ( !text && startsWithFiller( mutation.target ) ) {
189-
mutatedElements.add( domConverter.getCorrespondingViewElement( mutation.target.parentNode ) );
189+
mutatedElements.add( domConverter.mapDomToView( mutation.target.parentNode ) );
190190
}
191191
}
192192
}
@@ -203,7 +203,7 @@ export default class MutationObserver extends Observer {
203203
}
204204

205205
for ( const viewElement of mutatedElements ) {
206-
const domElement = domConverter.getCorrespondingDomElement( viewElement );
206+
const domElement = domConverter.mapViewToDom( viewElement );
207207
const viewChildren = viewElement.getChildren();
208208
const newViewChildren = domConverter.domChildrenToView( domElement );
209209

src/view/renderer.js

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ export default class Renderer {
127127
*/
128128
markToSync( type, node ) {
129129
if ( type === 'text' ) {
130-
if ( this.domConverter.getCorrespondingDom( node.parent ) ) {
130+
if ( this.domConverter.mapViewToDom( node.parent ) ) {
131131
this.markedTexts.add( node );
132132
}
133133
} else {
134134
// If the node has no DOM element it is not rendered yet,
135135
// its children/attributes do not need to be marked to be sync.
136-
if ( !this.domConverter.getCorrespondingDom( node ) ) {
136+
if ( !this.domConverter.mapViewToDom( node ) ) {
137137
return;
138138
}
139139

@@ -165,9 +165,9 @@ export default class Renderer {
165165
* attributes which do not exist in the view element.
166166
*
167167
* For text nodes it updates the text string if it is different. Note that if parent element is marked as an element
168-
* which changed child list, text node update will not be done, because it may not be possible do find a
169-
* {@link module:engine/view/domconverter~DomConverter#getCorrespondingDomText corresponding DOM text}. The change will be handled
170-
* in the parent element.
168+
* which changed child list, text node update will not be done, because it may not be possible to
169+
* {@link module:engine/view/domconverter~DomConverter#findCorrespondingDomText find a corresponding DOM text}.
170+
* The change will be handled in the parent element.
171171
*
172172
* For elements, which child lists have changed, it calculates a {@link module:utils/diff~diff} and adds or removes children which have
173173
* changed.
@@ -190,7 +190,7 @@ export default class Renderer {
190190
if ( this._inlineFiller ) {
191191
inlineFillerPosition = this._getInlineFillerPosition();
192192
}
193-
// Othewise, if it's needed, create it at the selection position.
193+
// Otherwise, if it's needed, create it at the selection position.
194194
else if ( this._needsInlineFillerAtSelection() ) {
195195
inlineFillerPosition = this.selection.getFirstPosition();
196196

@@ -199,7 +199,7 @@ export default class Renderer {
199199
}
200200

201201
for ( const node of this.markedTexts ) {
202-
if ( !this.markedChildren.has( node.parent ) && this.domConverter.getCorrespondingDom( node.parent ) ) {
202+
if ( !this.markedChildren.has( node.parent ) && this.domConverter.mapViewToDom( node.parent ) ) {
203203
this._updateText( node, { inlineFillerPosition } );
204204
}
205205
}
@@ -351,7 +351,7 @@ export default class Renderer {
351351
const selectionOffset = selectionPosition.offset;
352352

353353
// If there is no DOM root we do not care about fillers.
354-
if ( !this.domConverter.getCorrespondingDomElement( selectionParent.root ) ) {
354+
if ( !this.domConverter.mapViewToDom( selectionParent.root ) ) {
355355
return false;
356356
}
357357

@@ -384,7 +384,7 @@ export default class Renderer {
384384
* filler should be rendered.
385385
*/
386386
_updateText( viewText, options ) {
387-
const domText = this.domConverter.getCorrespondingDom( viewText );
387+
const domText = this.domConverter.findCorrespondingDomText( viewText );
388388
const newDomText = this.domConverter.viewToDom( viewText, domText.ownerDocument );
389389

390390
const actualText = domText.data;
@@ -408,7 +408,7 @@ export default class Renderer {
408408
* @param {module:engine/view/element~Element} viewElement View element to update.
409409
*/
410410
_updateAttrs( viewElement ) {
411-
const domElement = this.domConverter.getCorrespondingDom( viewElement );
411+
const domElement = this.domConverter.mapViewToDom( viewElement );
412412
const domAttrKeys = Array.from( domElement.attributes ).map( attr => attr.name );
413413
const viewAttrKeys = viewElement.getAttributeKeys();
414414

@@ -436,7 +436,7 @@ export default class Renderer {
436436
*/
437437
_updateChildren( viewElement, options ) {
438438
const domConverter = this.domConverter;
439-
const domElement = domConverter.getCorrespondingDom( viewElement );
439+
const domElement = domConverter.mapViewToDom( viewElement );
440440

441441
if ( !domElement ) {
442442
// If there is no `domElement` it means that it was already removed from DOM.
@@ -445,9 +445,7 @@ export default class Renderer {
445445
}
446446

447447
const domDocument = domElement.ownerDocument;
448-
449448
const filler = options.inlineFillerPosition;
450-
451449
const actualDomChildren = domElement.childNodes;
452450
const expectedDomChildren = Array.from( domConverter.viewChildrenToDom( viewElement, domDocument, { bind: true } ) );
453451

@@ -464,20 +462,29 @@ export default class Renderer {
464462
const actions = diff( actualDomChildren, expectedDomChildren, sameNodes );
465463

466464
let i = 0;
465+
const nodesToUnbind = new Set();
467466

468467
for ( const action of actions ) {
469468
if ( action === 'insert' ) {
470469
insertAt( domElement, i, expectedDomChildren[ i ] );
471470
i++;
472471
} else if ( action === 'delete' ) {
473-
// Whenever element is removed from DOM, unbind it and all of its children.
474-
this.domConverter.unbindDomElement( actualDomChildren[ i ] );
472+
nodesToUnbind.add( actualDomChildren[ i ] );
475473
remove( actualDomChildren[ i ] );
476474
} else { // 'equal'
477475
i++;
478476
}
479477
}
480478

479+
// Unbind removed nodes. When node does not have a parent it means that it was removed from DOM tree during
480+
// comparision with the expected DOM. We don't need to check child nodes, because if child node was reinserted,
481+
// it was moved to DOM tree out of the removed node.
482+
for ( const node of nodesToUnbind ) {
483+
if ( !node.parentNode ) {
484+
this.domConverter.unbindDomElement( node );
485+
}
486+
}
487+
481488
function sameNodes( actualDomChild, expectedDomChild ) {
482489
// Elements.
483490
if ( actualDomChild === expectedDomChild ) {
@@ -512,7 +519,7 @@ export default class Renderer {
512519
return;
513520
}
514521

515-
const domRoot = this.domConverter.getCorrespondingDomElement( this.selection.editableElement );
522+
const domRoot = this.domConverter.mapViewToDom( this.selection.editableElement );
516523

517524
// Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
518525
if ( !this.isFocused || !domRoot ) {
@@ -618,7 +625,7 @@ export default class Renderer {
618625

619626
if ( domSelection.rangeCount ) {
620627
const activeDomElement = doc.activeElement;
621-
const viewElement = this.domConverter.getCorrespondingViewElement( activeDomElement );
628+
const viewElement = this.domConverter.mapDomToView( activeDomElement );
622629

623630
if ( activeDomElement && viewElement ) {
624631
doc.getSelection().removeAllRanges();

tests/controller/editingcontroller.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe( 'EditingController', () => {
6666
expect( viewRoot ).to.equal( editing.view.getRoot() );
6767
expect( domRoot ).to.equal( editing.view.getDomRoot() );
6868

69-
expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
69+
expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
7070
expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
7171

7272
expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );
@@ -81,7 +81,7 @@ describe( 'EditingController', () => {
8181
expect( viewRoot ).to.equal( editing.view.getRoot( 'header' ) );
8282
expect( domRoot ).to.equal( editing.view.getDomRoot( 'header' ) );
8383

84-
expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
84+
expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
8585
expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
8686

8787
expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( model.getRoot( 'header' ) );
@@ -100,7 +100,7 @@ describe( 'EditingController', () => {
100100

101101
expect( domRoot ).to.equal( editing.view.getDomRoot() );
102102

103-
expect( editing.view.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domRoot );
103+
expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
104104
expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
105105

106106
expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( modelRoot );

tests/view/document/document.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe( 'Document', () => {
103103
expect( ret ).to.equal( viewRoot );
104104

105105
expect( domRoot ).to.equal( domDiv );
106-
expect( viewDocument.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
106+
expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
107107

108108
expect( viewRoot.name ).to.equal( 'div' );
109109
expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
@@ -185,7 +185,7 @@ describe( 'Document', () => {
185185
expect( count( viewDocument.roots ) ).to.equal( 1 );
186186

187187
expect( viewDocument.getDomRoot() ).to.equal( domDiv );
188-
expect( viewDocument.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
188+
expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
189189

190190
expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
191191
} );
@@ -205,7 +205,7 @@ describe( 'Document', () => {
205205
expect( count( viewDocument.roots ) ).to.equal( 2 );
206206

207207
expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
208-
expect( viewDocument.domConverter.getCorrespondingDom( viewH1 ) ).to.equal( domH1 );
208+
expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
209209

210210
expect( viewDocument.getRoot().name ).to.equal( 'div' );
211211
expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;

0 commit comments

Comments
 (0)