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

Commit 558638c

Browse files
author
Piotr Jasiun
authored
Merge pull request #1661 from ckeditor/t/1653-b
Fix: Prevented `View` from firing the `render` event if there were no changes since the last rendering. Closes #1653. Closes #1660. BREAKING CHANGE: The `editing.view.render()` method was renamed to `editing.view.forceRender()`. It should be used with caution as it will re-render editing view and repaint the UI.
2 parents c9816b3 + c2826ca commit 558638c

22 files changed

+185
-113
lines changed

src/controller/editingcontroller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,11 @@ export default class EditingController {
7575
//
7676
// See https://github.com/ckeditor/ckeditor5-engine/issues/1528
7777
this.listenTo( this.model, '_beforeChanges', () => {
78-
this.view._renderingDisabled = true;
78+
this.view._disableRendering( true );
7979
}, { priority: 'highest' } );
8080

81-
this.listenTo( this.model, '_afterChanges', ( evt, { hasModelDocumentChanged } ) => {
82-
this.view._renderingDisabled = false;
83-
if ( hasModelDocumentChanged ) {
84-
this.view.render();
85-
}
81+
this.listenTo( this.model, '_afterChanges', () => {
82+
this.view._disableRendering( false );
8683
}, { priority: 'lowest' } );
8784

8885
// Whenever model document is changed, convert those changes to the view (using model.Document#differ).

src/model/document.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ export default class Document {
240240
}
241241

242242
/**
243-
* Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features that listen to
244-
* the {@link module:engine/model/model~Model#event:_change model's change event} will operate on a correct model state.
243+
* Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
244+
* will operate on a correct model state.
245245
*
246246
* An execution of a feature may lead to an incorrect document tree state. The callbacks are used to fix the document tree after
247247
* it has changed. Post-fixers are fired just after all changes from the outermost change block were applied but

src/model/model.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,6 @@ export default class Model {
683683
*/
684684
_runPendingChanges() {
685685
const ret = [];
686-
let hasModelDocumentChanged = false;
687686

688687
this.fire( '_beforeChanges' );
689688

@@ -696,9 +695,6 @@ export default class Model {
696695
const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
697696
ret.push( callbackReturnValue );
698697

699-
// Collect an information whether the model document has changed during from the last pending change.
700-
hasModelDocumentChanged = hasModelDocumentChanged || this.document._hasDocumentChangedFromTheLastChangeBlock();
701-
702698
// Fire '_change' event before resetting differ.
703699
this.fire( '_change', this._currentWriter );
704700

@@ -708,7 +704,7 @@ export default class Model {
708704
this._currentWriter = null;
709705
}
710706

711-
this.fire( '_afterChanges', { hasModelDocumentChanged } );
707+
this.fire( '_afterChanges' );
712708

713709
return ret;
714710
}
@@ -739,8 +735,6 @@ export default class Model {
739735
*
740736
* @protected
741737
* @event _afterChanges
742-
* @param {Object} options
743-
* @param {Boolean} options.hasModelDocumentChanged `true` if the model document has changed during the
744738
* {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} blocks.
745739
*/
746740

src/view/document.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export default class Document {
115115
* As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}
116116
* instance connected with the executed changes block.
117117
*
118+
* Note that registering a post-fixer won't re-render the editor's view. If the view should change after registering the post-fixer then
119+
* it should be done manually calling `view.forceRender();`.
120+
*
118121
* @param {Function} postFixer
119122
*/
120123
registerPostFixer( postFixer ) {

src/view/observer/focusobserver.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default class FocusObserver extends DomEventObserver {
3737
// overwrite new DOM selection with selection from the view.
3838
// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
3939
// Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
40-
this._renderTimeoutId = setTimeout( () => view.render(), 50 );
40+
this._renderTimeoutId = setTimeout( () => view.forceRender(), 50 );
4141
} );
4242

4343
document.on( 'blur', ( evt, data ) => {
@@ -47,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
4747
document.isFocused = false;
4848

4949
// Re-render the document to update view elements.
50-
view.render();
50+
view.forceRender();
5151
}
5252
} );
5353

src/view/observer/mutationobserver.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export default class MutationObserver extends Observer {
248248

249249
// If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched
250250
// view (which has not been changed). In order to "reset DOM" we render the view again.
251-
this.view.render();
251+
this.view.forceRender();
252252

253253
function sameNodes( child1, child2 ) {
254254
// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.

src/view/observer/selectionobserver.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export default class SelectionObserver extends Observer {
166166
if ( this.selection.isSimilar( newViewSelection ) ) {
167167
// If selection was equal and we are at this point of algorithm, it means that it was incorrect.
168168
// Just re-render it, no need to fire any events, etc.
169-
this.view.render();
169+
this.view.forceRender();
170170
} else {
171171
const data = {
172172
oldSelection: this.selection,

src/view/placeholder.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ export function attachPlaceholder( view, element, placeholderText, checkFunction
3939
checkFunction
4040
} );
4141

42-
// Update view right away.
43-
view.render();
42+
view.change( writer => updateAllPlaceholders( document, writer ) );
4443
}
4544

4645
/**

src/view/view.js

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default class View {
6666
* Instance of the {@link module:engine/view/document~Document} associated with this view controller.
6767
*
6868
* @readonly
69-
* @member {module:engine/view/document~Document} module:engine/view/view~View#document
69+
* @type {module:engine/view/document~Document}
7070
*/
7171
this.document = new Document();
7272

@@ -76,15 +76,15 @@ export default class View {
7676
* and {@link module:engine/view/observer/observer~Observer observers}.
7777
*
7878
* @readonly
79-
* @member {module:engine/view/domconverter~DomConverter} module:engine/view/view~View#domConverter
79+
* @type {module:engine/view/domconverter~DomConverter}
8080
*/
8181
this.domConverter = new DomConverter();
8282

8383
/**
8484
* Instance of the {@link module:engine/view/renderer~Renderer renderer}.
8585
*
8686
* @protected
87-
* @member {module:engine/view/renderer~Renderer} module:engine/view/view~View#renderer
87+
* @type {module:engine/view/renderer~Renderer}
8888
*/
8989
this._renderer = new Renderer( this.domConverter, this.document.selection );
9090
this._renderer.bind( 'isFocused' ).to( this.document );
@@ -93,55 +93,64 @@ export default class View {
9393
* Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
9494
*
9595
* @readonly
96-
* @member {Map} module:engine/view/view~View#domRoots
96+
* @type {Map.<String, HTMLElement>}
9797
*/
9898
this.domRoots = new Map();
9999

100100
/**
101101
* Map of registered {@link module:engine/view/observer/observer~Observer observers}.
102102
*
103103
* @private
104-
* @member {Map.<Function, module:engine/view/observer/observer~Observer>} module:engine/view/view~View#_observers
104+
* @type {Map.<Function, module:engine/view/observer/observer~Observer>}
105105
*/
106106
this._observers = new Map();
107107

108108
/**
109109
* Is set to `true` when {@link #change view changes} are currently in progress.
110110
*
111111
* @private
112-
* @member {Boolean} module:engine/view/view~View#_ongoingChange
112+
* @type {Boolean}
113113
*/
114114
this._ongoingChange = false;
115115

116116
/**
117117
* Used to prevent calling {@link #render} and {@link #change} during rendering view to the DOM.
118118
*
119119
* @private
120-
* @member {Boolean} module:engine/view/view~View#_renderingInProgress
120+
* @type {Boolean}
121121
*/
122122
this._renderingInProgress = false;
123123

124124
/**
125125
* Used to prevent calling {@link #render} and {@link #change} during rendering view to the DOM.
126126
*
127127
* @private
128-
* @member {Boolean} module:engine/view/view~View#_renderingInProgress
128+
* @type {Boolean}
129129
*/
130130
this._postFixersInProgress = false;
131131

132132
/**
133-
* Internal flag to temporary disable rendering. See usage in the editing controller.
133+
* Internal flag to temporary disable rendering. See the usage in the {@link #_disableRendering}.
134134
*
135-
* @protected
136-
* @member {Boolean} module:engine/view/view~View#_renderingDisabled
135+
* @private
136+
* @type {Boolean}
137137
*/
138138
this._renderingDisabled = false;
139139

140140
/**
141-
* DowncastWriter instance used in {@link #change change method) callbacks.
141+
* Internal flag that disables rendering when there are no changes since the last rendering.
142+
* It stores information about changed selection and changed elements from attached document roots.
143+
*
144+
* @private
145+
* @type {Boolean}
146+
*/
147+
this._hasChangedSinceTheLastRendering = false;
148+
149+
/**
150+
* DowncastWriter instance used in {@link #change change method} callbacks.
142151
*
143152
* @private
144-
* @member {module:engine/view/downcastwriter~DowncastWriter} module:engine/view/view~View#_writer
153+
* @type {module:engine/view/downcastwriter~DowncastWriter}
145154
*/
146155
this._writer = new DowncastWriter( this.document );
147156

@@ -163,6 +172,14 @@ export default class View {
163172

164173
// Informs that layout has changed after render.
165174
this.document.fire( 'layoutChanged' );
175+
176+
// Reset the `_hasChangedSinceTheLastRendering` flag after rendering.
177+
this._hasChangedSinceTheLastRendering = false;
178+
} );
179+
180+
// Listen to the document selection changes directly.
181+
this.listenTo( this.document.selection, 'change', () => {
182+
this._hasChangedSinceTheLastRendering = true;
166183
} );
167184
}
168185

@@ -192,6 +209,10 @@ export default class View {
192209
viewRoot.on( 'change:attributes', ( evt, node ) => this._renderer.markToSync( 'attributes', node ) );
193210
viewRoot.on( 'change:text', ( evt, node ) => this._renderer.markToSync( 'text', node ) );
194211

212+
viewRoot.on( 'change', () => {
213+
this._hasChangedSinceTheLastRendering = true;
214+
} );
215+
195216
for ( const observer of this._observers.values() ) {
196217
observer.observe( domRoot, name );
197218
}
@@ -293,7 +314,7 @@ export default class View {
293314

294315
if ( editable ) {
295316
this.domConverter.focus( editable );
296-
this.render();
317+
this.forceRender();
297318
} else {
298319
/**
299320
* Before focusing view document, selection should be placed inside one of the view's editables.
@@ -309,9 +330,10 @@ export default class View {
309330

310331
/**
311332
* The `change()` method is the primary way of changing the view. You should use it to modify any node in the view tree.
312-
* It makes sure that after all changes are made the view is rendered to the DOM. It prevents situations when the DOM is updated
313-
* when the view state is not yet correct. It allows to nest calls one inside another and still performs a single rendering
314-
* after all those changes are made. It also returns the return value of its callback.
333+
* It makes sure that after all changes are made the view is rendered to the DOM (assuming that the view will be changed
334+
* inside the callback). It prevents situations when the DOM is updated when the view state is not yet correct. It allows
335+
* to nest calls one inside another and still performs a single rendering after all those changes are made.
336+
* It also returns the return value of its callback.
315337
*
316338
* const text = view.change( writer => {
317339
* const newText = writer.createText( 'foo' );
@@ -368,7 +390,8 @@ export default class View {
368390

369391
// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
370392
// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all changes.
371-
if ( !this._renderingDisabled ) {
393+
// Also, we don't need to render anything if there're no changes since last rendering.
394+
if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) {
372395
this._postFixersInProgress = true;
373396
this.document._callPostFixers( this._writer );
374397
this._postFixersInProgress = false;
@@ -380,13 +403,17 @@ export default class View {
380403
}
381404

382405
/**
383-
* Renders {@link module:engine/view/document~Document view document} to DOM. If any view changes are
406+
* Forces rendering {@link module:engine/view/document~Document view document} to DOM. If any view changes are
384407
* currently in progress, rendering will start after all {@link #change change blocks} are processed.
385408
*
409+
* Note that this method is dedicated for special cases. All view changes should be wrapped in the {@link #change}
410+
* block and the view will automatically check whether it needs to render DOM or not.
411+
*
386412
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
387413
* trying to re-render when rendering to DOM has already started.
388414
*/
389-
render() {
415+
forceRender() {
416+
this._hasChangedSinceTheLastRendering = true;
390417
this.change( () => {} );
391418
}
392419

@@ -542,6 +569,22 @@ export default class View {
542569
return new Selection( selectable, placeOrOffset, options );
543570
}
544571

572+
/**
573+
* Disables or enables rendering. If the flag is set to `true` then the rendering will be disabled.
574+
* If the flag is set to `false` and if there was some change in the meantime, then the rendering action will be performed.
575+
*
576+
* @protected
577+
* @param {Boolean} flag A flag indicates whether the rendering should be disabled.
578+
*/
579+
_disableRendering( flag ) {
580+
this._renderingDisabled = flag;
581+
582+
if ( flag == false ) {
583+
// Render when you stop blocking rendering.
584+
this.change( () => {} );
585+
}
586+
}
587+
545588
/**
546589
* Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
547590
* before rendering and re-enabled after that.

tests/controller/editingcontroller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ describe( 'EditingController', () => {
186186
} );
187187

188188
editing.view.document.isFocused = true;
189-
editing.view.render();
189+
editing.view.forceRender();
190190

191191
const domSelection = document.getSelection();
192192
domSelection.removeAllRanges();

0 commit comments

Comments
 (0)