@@ -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.
0 commit comments