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

Commit 63b9d14

Browse files
author
Piotr Jasiun
authored
Merge pull request #1313 from ckeditor/t/1312
Other: Fix `render()` and `change()` flow. Introduce postfixers in view. Closes #1312.
2 parents e70ab96 + 3cd9d1f commit 63b9d14

File tree

8 files changed

+304
-209
lines changed

8 files changed

+304
-209
lines changed

src/conversion/downcastdispatcher.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,6 @@ export default class DowncastDispatcher {
476476
* @param {Object} data Additional information about the change.
477477
* @param {module:engine/model/item~Item} data.item Inserted item.
478478
* @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
479-
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
480479
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
481480
*/
482481

src/view/document.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export default class Document {
6565
* @member {Boolean} module:engine/view/document~Document#isFocused
6666
*/
6767
this.set( 'isFocused', false );
68+
69+
/**
70+
* Post-fixer callbacks registered to the view document.
71+
*
72+
* @private
73+
* @member {Set}
74+
*/
75+
this._postFixers = new Set();
6876
}
6977

7078
/**
@@ -78,6 +86,44 @@ export default class Document {
7886
getRoot( name = 'main' ) {
7987
return this.roots.get( name );
8088
}
89+
90+
/**
91+
* Used to register a post-fixer callback. A post-fixers mechanism allows to update view tree just before rendering
92+
* to the DOM.
93+
*
94+
* Post-fixers are fired just after all changes from the outermost change block were applied but
95+
* before the {@link module:engine/view/view~View#event:render render event} is fired. If a post-fixer callback made
96+
* a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
97+
* not be fixed in the new document tree state.
98+
*
99+
* As a parameter, a post-fixer callback receives a {@link module:engine/view/writer~Writer writer} instance connected with the
100+
* executed changes block.
101+
*
102+
* @param {Function} postFixer
103+
*/
104+
registerPostFixer( postFixer ) {
105+
this._postFixers.add( postFixer );
106+
}
107+
108+
/**
109+
* Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
110+
*
111+
* @protected
112+
* @param {module:engine/view/writer~Writer} writer
113+
*/
114+
_callPostFixers( writer ) {
115+
let wasFixed = false;
116+
117+
do {
118+
for ( const callback of this._postFixers ) {
119+
wasFixed = callback( writer );
120+
121+
if ( wasFixed ) {
122+
break;
123+
}
124+
}
125+
} while ( wasFixed );
126+
}
81127
}
82128

83129
mix( Document, ObservableMixin );

src/view/placeholder.js

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,16 @@
77
* @module engine/view/placeholder
88
*/
99

10-
import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
11-
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
1210
import '../../theme/placeholder.css';
1311

14-
const listener = {};
15-
extend( listener, EmitterMixin );
16-
1712
// Each document stores information about its placeholder elements and check functions.
1813
const documentPlaceholders = new WeakMap();
1914

2015
/**
2116
* Attaches placeholder to provided element and updates it's visibility. To change placeholder simply call this method
2217
* once again with new parameters.
2318
*
19+
* @param {module:engine/view/view~View} view View controller.
2420
* @param {module:engine/view/element~Element} element Element to attach placeholder to.
2521
* @param {String} placeholderText Placeholder text to use.
2622
* @param {Function} [checkFunction] If provided it will be called before checking if placeholder should be displayed.
@@ -29,28 +25,19 @@ const documentPlaceholders = new WeakMap();
2925
export function attachPlaceholder( view, element, placeholderText, checkFunction ) {
3026
const document = view.document;
3127

32-
// Detach placeholder if was used before.
33-
detachPlaceholder( view, element );
34-
3528
// Single listener per document.
3629
if ( !documentPlaceholders.has( document ) ) {
3730
documentPlaceholders.set( document, new Map() );
3831

39-
// Attach listener just before rendering and update placeholders.
40-
listener.listenTo( view, 'render', () => updateAllPlaceholders( view ) );
32+
// Create view post fixer that will add placeholder where needed.
33+
document.registerPostFixer( writer => updateAllPlaceholders( document, writer ) );
4134
}
4235

43-
// Store text in element's data attribute.
44-
// This data attribute is used in CSS class to show the placeholder.
45-
view.change( writer => {
46-
writer.setAttribute( 'data-placeholder', placeholderText, element );
47-
} );
48-
49-
// Store information about placeholder.
50-
documentPlaceholders.get( document ).set( element, checkFunction );
36+
// Store information about element with placeholder.
37+
documentPlaceholders.get( document ).set( element, { placeholderText, checkFunction } );
5138

52-
// Update right away too.
53-
updateSinglePlaceholder( view, element, checkFunction );
39+
// Update view right away.
40+
view.render();
5441
}
5542

5643
/**
@@ -75,39 +62,55 @@ export function detachPlaceholder( view, element ) {
7562
// Updates all placeholders of given document.
7663
//
7764
// @private
78-
// @param {module:engine/view/view~View} view
79-
function updateAllPlaceholders( view ) {
80-
const placeholders = documentPlaceholders.get( view.document );
81-
82-
for ( const [ element, checkFunction ] of placeholders ) {
83-
updateSinglePlaceholder( view, element, checkFunction );
65+
// @param {module:engine/view/document~Document} view
66+
// @param {module:engine/view/writer~Writer} writer
67+
function updateAllPlaceholders( document, writer ) {
68+
const placeholders = documentPlaceholders.get( document );
69+
let changed = false;
70+
71+
for ( const [ element, info ] of placeholders ) {
72+
if ( updateSinglePlaceholder( writer, element, info ) ) {
73+
changed = true;
74+
}
8475
}
76+
77+
return changed;
8578
}
8679

8780
// Updates placeholder class of given element.
8881
//
8982
// @private
90-
// @param {module:engine/view/view~View} view
83+
// @param {module:engine/view/writer~Writer} writer
9184
// @param {module:engine/view/element~Element} element
92-
// @param {Function} checkFunction
93-
function updateSinglePlaceholder( view, element, checkFunction ) {
85+
// @param {Object} info
86+
function updateSinglePlaceholder( writer, element, info ) {
9487
const document = element.document;
88+
const text = info.placeholderText;
89+
let changed = false;
9590

9691
// Element was removed from document.
9792
if ( !document ) {
98-
return;
93+
return false;
94+
}
95+
96+
// Update data attribute if needed.
97+
if ( element.getAttribute( 'data-placeholder' ) !== text ) {
98+
writer.setAttribute( 'data-placeholder', text, element );
99+
changed = true;
99100
}
100101

101102
const viewSelection = document.selection;
102103
const anchor = viewSelection.anchor;
104+
const checkFunction = info.checkFunction;
103105

104106
// If checkFunction is provided and returns false - remove placeholder.
105107
if ( checkFunction && !checkFunction() ) {
106-
view.change( writer => {
108+
if ( element.hasClass( 'ck-placeholder' ) ) {
107109
writer.removeClass( 'ck-placeholder', element );
108-
} );
110+
changed = true;
111+
}
109112

110-
return;
113+
return changed;
111114
}
112115

113116
// Element is empty for placeholder purposes when it has no children or only ui elements.
@@ -116,21 +119,26 @@ function updateSinglePlaceholder( view, element, checkFunction ) {
116119

117120
// If element is empty and editor is blurred.
118121
if ( !document.isFocused && isEmptyish ) {
119-
view.change( writer => {
122+
if ( !element.hasClass( 'ck-placeholder' ) ) {
120123
writer.addClass( 'ck-placeholder', element );
121-
} );
124+
changed = true;
125+
}
122126

123-
return;
127+
return changed;
124128
}
125129

126130
// It there are no child elements and selection is not placed inside element.
127131
if ( isEmptyish && anchor && anchor.parent !== element ) {
128-
view.change( writer => {
132+
if ( !element.hasClass( 'ck-placeholder' ) ) {
129133
writer.addClass( 'ck-placeholder', element );
130-
} );
134+
changed = true;
135+
}
131136
} else {
132-
view.change( writer => {
137+
if ( element.hasClass( 'ck-placeholder' ) ) {
133138
writer.removeClass( 'ck-placeholder', element );
134-
} );
139+
changed = true;
140+
}
135141
}
142+
143+
return changed;
136144
}

src/view/view.js

Lines changed: 52 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,20 @@ export default class View {
109109
this._ongoingChange = false;
110110

111111
/**
112-
* Is set to `true` when rendering view to DOM was started.
113-
* This is used to check whether view document can accept changes in current state.
114-
* From the moment when rendering to DOM is stared view tree is locked to prevent changes that will not be
115-
* reflected in the DOM.
112+
* Used to prevent calling {@link #render} and {@link #change) during rendering view to the DOM.
116113
*
117114
* @private
118-
* @member {Boolean} module:engine/view/view~View#_renderingStarted
115+
* @member {Boolean} module:engine/view/view~View#_renderingInProgress
119116
*/
120-
this._renderingStarted = false;
117+
this._renderingInProgress = false;
118+
119+
/**
120+
* Used to prevent calling {@link #render} and {@link #change) during rendering view to the DOM.
121+
*
122+
* @private
123+
* @member {Boolean} module:engine/view/view~View#_renderingInProgress
124+
*/
125+
this._postFixersInProgress = false;
121126

122127
/**
123128
* Writer instance used in {@link #change change method) callbacks.
@@ -138,10 +143,10 @@ export default class View {
138143
injectQuirksHandling( this );
139144
injectUiElementHandling( this );
140145

141-
// Use 'low` priority so that all listeners on 'normal` priority will be executed before.
146+
// Use 'normal' priority so that rendering is performed as first when using that priority.
142147
this.on( 'render', () => {
143148
this._render();
144-
}, { priority: 'low' } );
149+
} );
145150
}
146151

147152
/**
@@ -312,21 +317,41 @@ export default class View {
312317
* @param {Function} callback Callback function which may modify the view.
313318
*/
314319
change( callback ) {
315-
// Check if change is performed in correct moment.
316-
this._assertRenderingInProgress();
320+
if ( this._renderingInProgress || this._postFixersInProgress ) {
321+
/**
322+
* Thrown when there is an attempt to make changes to the view tree when it is in incorrect state. This may
323+
* cause some unexpected behaviour and inconsistency between the DOM and the view.
324+
* This may be caused by:
325+
* * calling {@link #change} or {@link #render} during rendering process,
326+
* * calling {@link #change} or {@link #render} inside of
327+
* {@link module:engine/view/document~Document#registerPostFixer post fixer function}.
328+
*/
329+
throw new CKEditorError(
330+
'cannot-change-view-tree: ' +
331+
'Attempting to make changes to the view when it is in incorrect state: rendering or post fixers are in progress. ' +
332+
'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
333+
);
334+
}
317335

318-
// If other changes are in progress wait with rendering until every ongoing change is over.
336+
// Recursive call to view.change() method - execute listener immediately.
319337
if ( this._ongoingChange ) {
320338
callback( this._writer );
321-
} else {
322-
this._ongoingChange = true;
323-
324-
callback( this._writer );
325-
this.fire( 'render' );
326339

327-
this._ongoingChange = false;
328-
this._renderingStarted = false;
340+
return;
329341
}
342+
343+
// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
344+
// event for all nested calls.
345+
this._ongoingChange = true;
346+
callback( this._writer );
347+
this._ongoingChange = false;
348+
349+
// Execute all document post fixers after the change.
350+
this._postFixersInProgress = true;
351+
this.document._callPostFixers( this._writer );
352+
this._postFixersInProgress = false;
353+
354+
this.fire( 'render' );
330355
}
331356

332357
/**
@@ -337,15 +362,7 @@ export default class View {
337362
* trying to re-render when rendering to DOM has already started.
338363
*/
339364
render() {
340-
// Check if rendering is performed in correct moment.
341-
this._assertRenderingInProgress();
342-
343-
// Render only if no ongoing changes are in progress. If there are some, view document will be rendered after all
344-
// changes are done. This way view document will not be rendered in the middle of some changes.
345-
if ( !this._ongoingChange ) {
346-
this.fire( 'render' );
347-
this._renderingStarted = false;
348-
}
365+
this.change( () => {} );
349366
}
350367

351368
/**
@@ -366,47 +383,22 @@ export default class View {
366383
* @private
367384
*/
368385
_render() {
369-
this._renderingStarted = true;
370-
386+
this._renderingInProgress = true;
371387
this.disableObservers();
372388
this._renderer.render();
373389
this.enableObservers();
390+
this._renderingInProgress = false;
374391
}
375392

376393
/**
377-
* Throws `applying-view-changes-on-rendering` error when trying to modify or re-render view tree when rendering is
378-
* already started
394+
* Fired after a topmost {@link module:engine/view/view~View#change change block} and all
395+
* {@link module:engine/view/document~Document#registerPostFixer post fixers} are executed.
379396
*
380-
* @private
381-
*/
382-
_assertRenderingInProgress() {
383-
if ( this._renderingStarted ) {
384-
/**
385-
* There is an attempt to make changes in the view tree after the rendering process
386-
* has started. This may cause unexpected behaviour and inconsistency between the DOM and the view.
387-
* This may be caused by:
388-
* * calling `view.change()` or `view.render()` methods during rendering process,
389-
* * calling `view.change()` or `view.render()` methods in callbacks to
390-
* {module:engine/view/document~Document#event:change view document change event) on `low` priority, after
391-
* rendering is over for current `change` block.
392-
*
393-
* @error applying-view-changes-on-rendering
394-
*/
395-
throw new CKEditorError(
396-
'applying-view-changes-on-rendering: ' +
397-
'Attempting to make changes in the view during rendering process. ' +
398-
'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
399-
);
400-
}
401-
}
402-
403-
/**
404-
* Fired after a topmost {@link module:engine/view/view~View#change change block} is finished and the DOM rendering has
405-
* been executed.
397+
* Actual rendering is performed as a first listener on 'normal' priority.
406398
*
407-
* Actual rendering is performed on 'low' priority. This means that all listeners on 'normal' and above priorities
408-
* will be executed after changes made to view tree but before rendering to the DOM. Use `low` priority for callbacks that
409-
* should be executed after rendering to the DOM.
399+
* view.on( 'render', () => {
400+
* // Rendering to the DOM is complete.
401+
* } );
410402
*
411403
* @event module:engine/view/view~View#event:render
412404
*/

0 commit comments

Comments
 (0)