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

Commit b527d7f

Browse files
author
Piotr Jasiun
authored
Merge pull request #1220 from ckeditor/t/1211
Other: Operations that do not operate on a document should have `baseVersion` set to `null`. Closes #1211. Fixed: Markers again are properly converted in `engine.controller.DataController`. Fixed: Markers are cleared now before an operation is applied to `model.Document` tree to fix scenarios where marker range could not be converted to the view after the model changed.
2 parents ee88389 + 13bc98c commit b527d7f

34 files changed

+627
-376
lines changed

src/controller/datacontroller.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,10 @@ export default class DataController {
135135
* @returns {String} Output data.
136136
*/
137137
stringify( modelElementOrFragment ) {
138-
// model -> view
138+
// Model -> view.
139139
const viewDocumentFragment = this.toView( modelElementOrFragment );
140140

141-
// view -> data
141+
// View -> data.
142142
return this.processor.toData( viewDocumentFragment );
143143
}
144144

@@ -153,13 +153,25 @@ export default class DataController {
153153
* @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
154154
*/
155155
toView( modelElementOrFragment ) {
156+
// First, convert elements.
156157
const modelRange = ModelRange.createIn( modelElementOrFragment );
157158

158159
const viewDocumentFragment = new ViewDocumentFragment();
159160
this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
160161

161162
this.modelToView.convertInsert( modelRange );
162163

164+
if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
165+
// Then, if a document element is converted, convert markers.
166+
// From all document markers, get those, which "intersect" with the converter element.
167+
const markers = _getMarkersRelativeToElement( modelElementOrFragment );
168+
169+
for ( const [ name, range ] of markers ) {
170+
this.modelToView.convertMarkerAdd( name, range );
171+
}
172+
}
173+
174+
// Clear bindings so the next call to this method gives correct results.
163175
this.mapper.clearBindings();
164176

165177
return viewDocumentFragment;
@@ -234,3 +246,28 @@ export default class DataController {
234246
}
235247

236248
mix( DataController, ObservableMixin );
249+
250+
// Helper function for converting part of a model to view.
251+
//
252+
// Takes a document element (element that is added to a model document) and checks which markers are inside it
253+
// and which markers are containing it. If the marker is intersecting with element, the intersection is returned.
254+
function _getMarkersRelativeToElement( element ) {
255+
const result = [];
256+
const doc = element.root.document;
257+
258+
if ( !doc ) {
259+
return [];
260+
}
261+
262+
const elementRange = ModelRange.createIn( element );
263+
264+
for ( const marker of doc.model.markers ) {
265+
const intersection = elementRange.getIntersection( marker.getRange() );
266+
267+
if ( intersection ) {
268+
result.push( [ marker.name, intersection ] );
269+
}
270+
}
271+
272+
return result;
273+
}

src/controller/editingcontroller.js

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,12 @@ export default class EditingController {
8585

8686
const doc = this.model.document;
8787

88-
// When all changes are done, get the model diff containing all the changes and convert them to view and then render to DOM.
8988
this.listenTo( doc, 'change', () => {
90-
// Convert changes stored in `modelDiffer`.
9189
this.modelToView.convertChanges( doc.differ );
90+
}, { priority: 'low' } );
9291

93-
// After the view is ready, convert selection from model to view.
92+
this.listenTo( model, '_change', () => {
9493
this.modelToView.convertSelection( doc.selection );
95-
96-
// When everything is converted to the view, render it to DOM.
9794
this.view.render();
9895
}, { priority: 'low' } );
9996

@@ -110,6 +107,51 @@ export default class EditingController {
110107
this.modelToView.on( 'selection', convertRangeSelection(), { priority: 'low' } );
111108
this.modelToView.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
112109

110+
// Convert markers removal.
111+
//
112+
// Markers should be removed from the view before changes to the model are applied. This is because otherwise
113+
// it would be impossible to map some markers to the view (if, for example, the marker's boundary parent got removed).
114+
//
115+
// `removedMarkers` keeps information which markers already has been removed to prevent removing them twice.
116+
const removedMarkers = new Set();
117+
118+
this.listenTo( model, 'applyOperation', ( evt, args ) => {
119+
// Before operation is applied...
120+
const operation = args[ 0 ];
121+
122+
for ( const marker of model.markers ) {
123+
// Check all markers, that aren't already removed...
124+
if ( removedMarkers.has( marker.name ) ) {
125+
continue;
126+
}
127+
128+
const markerRange = marker.getRange();
129+
130+
if ( _operationAffectsMarker( operation, marker ) ) {
131+
// And if the operation in any way modifies the marker, remove the marker from the view.
132+
removedMarkers.add( marker.name );
133+
this.modelToView.convertMarkerRemove( marker.name, markerRange );
134+
135+
// TODO: This stinks but this is the safest place to have this code.
136+
this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
137+
}
138+
}
139+
}, { priority: 'high' } );
140+
141+
// If a marker is removed through `model.Model#markers` directly (not through operation), just remove it (if
142+
// it was not removed already).
143+
this.listenTo( model.markers, 'remove', ( evt, marker ) => {
144+
if ( !removedMarkers.has( marker.name ) ) {
145+
removedMarkers.add( marker.name );
146+
this.modelToView.convertMarkerRemove( marker.name, marker.getRange() );
147+
}
148+
} );
149+
150+
// When all changes are done, clear `removedMarkers` set.
151+
this.listenTo( model, '_change', () => {
152+
removedMarkers.clear();
153+
}, { priority: 'low' } );
154+
113155
// Binds {@link module:engine/view/document~Document#roots view roots collection} to
114156
// {@link module:engine/model/document~Document#roots model roots collection} so creating
115157
// model root automatically creates corresponding view root.
@@ -140,3 +182,23 @@ export default class EditingController {
140182
}
141183

142184
mix( EditingController, ObservableMixin );
185+
186+
// Helper function which checks whether given operation will affect given marker after the operation is applied.
187+
function _operationAffectsMarker( operation, marker ) {
188+
const range = marker.getRange();
189+
190+
if ( operation.type == 'insert' || operation.type == 'rename' ) {
191+
return _positionAffectsRange( operation.position, range );
192+
} else if ( operation.type == 'move' || operation.type == 'remove' || operation.type == 'reinsert' ) {
193+
return _positionAffectsRange( operation.targetPosition, range ) || _positionAffectsRange( operation.sourcePosition, range );
194+
} else if ( operation.type == 'marker' && operation.name == marker.name ) {
195+
return true;
196+
}
197+
198+
return false;
199+
}
200+
201+
// Helper function which checks whether change at given position affects given range.
202+
function _positionAffectsRange( position, range ) {
203+
return range.containsPosition( position ) || !range.start._getTransformedByInsertion( position, 1, true ).isEqual( range.start );
204+
}

src/conversion/modelconversiondispatcher.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,6 @@ export default class ModelConversionDispatcher {
128128
* @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
129129
*/
130130
convertChanges( differ ) {
131-
// First, before changing view structure, remove all markers that has changed.
132-
for ( const change of differ.getMarkersToRemove() ) {
133-
this.convertMarkerRemove( change.name, change.range );
134-
}
135-
136131
// Convert changes that happened on model tree.
137132
for ( const entry of differ.getChanges() ) {
138133
if ( entry.type == 'insert' ) {

src/model/batch.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,20 @@ export default class Batch {
5050
}
5151

5252
/**
53-
* Returns this batch base version, which is equal to the base version of first delta in the batch.
54-
* If there are no deltas in the batch, it returns `null`.
53+
* Returns this batch base version, which is equal to the base version of first delta (which has base version set)
54+
* in the batch. If there are no deltas in the batch or neither delta has base version set, it returns `null`.
5555
*
5656
* @readonly
5757
* @type {Number|null}
5858
*/
5959
get baseVersion() {
60-
return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
60+
for ( const delta of this.deltas ) {
61+
if ( delta.baseVersion !== null ) {
62+
return delta.baseVersion;
63+
}
64+
}
65+
66+
return null;
6167
}
6268

6369
/**

src/model/operation/attributeoperation.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export default class AttributeOperation extends Operation {
3737
* @param {String} key Key of an attribute to change or remove.
3838
* @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
3939
* @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
40-
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
40+
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
41+
* can be applied or `null` if the operation operates on detached (non-document) tree.
4142
*/
4243
constructor( range, key, oldValue, newValue, baseVersion ) {
4344
super( baseVersion );
@@ -73,11 +74,6 @@ export default class AttributeOperation extends Operation {
7374
* @member {*}
7475
*/
7576
this.newValue = newValue === undefined ? null : newValue;
76-
77-
/**
78-
* @inheritDoc
79-
*/
80-
this.isDocumentOperation = !!this.range.root.document;
8177
}
8278

8379
/**

src/model/operation/detachoperation.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ export default class DetachOperation extends Operation {
2727
* Position before the first {@link module:engine/model/item~Item model item} to move.
2828
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
2929
* `sourcePosition` with offset shifted by `howMany`.
30-
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
3130
*/
32-
constructor( sourcePosition, howMany, baseVersion ) {
33-
super( baseVersion );
31+
constructor( sourcePosition, howMany ) {
32+
super( null );
3433

3534
/**
3635
* Position before the first {@link module:engine/model/item~Item model item} to detach.
@@ -45,11 +44,6 @@ export default class DetachOperation extends Operation {
4544
* @member {Number} #howMany
4645
*/
4746
this.howMany = howMany;
48-
49-
/**
50-
* @inheritDoc
51-
*/
52-
this.isDocumentOperation = false;
5347
}
5448

5549
/**

src/model/operation/insertoperation.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export default class InsertOperation extends Operation {
2727
*
2828
* @param {module:engine/model/position~Position} position Position of insertion.
2929
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
30-
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
30+
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
31+
* can be applied or `null` if the operation operates on detached (non-document) tree.
3132
*/
3233
constructor( position, nodes, baseVersion ) {
3334
super( baseVersion );
@@ -47,11 +48,6 @@ export default class InsertOperation extends Operation {
4748
* @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
4849
*/
4950
this.nodes = new NodeList( _normalizeNodes( nodes ) );
50-
51-
/**
52-
* @inheritDoc
53-
*/
54-
this.isDocumentOperation = !!this.position.root.document;
5551
}
5652

5753
/**

src/model/operation/markeroperation.js

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export default class MarkerOperation extends Operation {
1919
* @param {module:engine/model/range~Range} oldRange Marker range before the change.
2020
* @param {module:engine/model/range~Range} newRange Marker range after the change.
2121
* @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
22-
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
22+
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
23+
* can be applied or `null` if the operation operates on detached (non-document) tree.
2324
*/
2425
constructor( name, oldRange, newRange, markers, baseVersion ) {
2526
super( baseVersion );
@@ -55,11 +56,6 @@ export default class MarkerOperation extends Operation {
5556
* @member {module:engine/model/markercollection~MarkerCollection}
5657
*/
5758
this._markers = markers;
58-
59-
/**
60-
* @inheritDoc
61-
*/
62-
this.isDocumentOperation = this._isDocumentOperation();
6359
}
6460

6561
/**
@@ -69,24 +65,6 @@ export default class MarkerOperation extends Operation {
6965
return 'marker';
7066
}
7167

72-
/**
73-
* Checks if operation is executed on document or document fragment nodes.
74-
*
75-
* @private
76-
*/
77-
_isDocumentOperation() {
78-
if ( this.newRange ) {
79-
return !!this.newRange.root.document;
80-
}
81-
82-
if ( this.oldRange ) {
83-
return !!this.oldRange.root.document;
84-
}
85-
86-
// This is edge and might happen only on data from the server.
87-
return true;
88-
}
89-
9068
/**
9169
* Creates and returns an operation that has the same parameters as this operation.
9270
*

src/model/operation/moveoperation.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export default class MoveOperation extends Operation {
2929
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
3030
* `sourcePosition` with offset shifted by `howMany`.
3131
* @param {module:engine/model/position~Position} targetPosition Position at which moved nodes will be inserted.
32-
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
32+
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
33+
* can be applied or `null` if the operation operates on detached (non-document) tree.
3334
*/
3435
constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
3536
super( baseVersion );
@@ -64,17 +65,6 @@ export default class MoveOperation extends Operation {
6465
* @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky
6566
*/
6667
this.isSticky = false;
67-
68-
/**
69-
* Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
70-
*
71-
* Note that range cannot be moved within different documents e.g. from docFrag to document root so
72-
* root of source and target positions is always the same.
73-
*
74-
* @readonly
75-
* @member {Boolean} #isDocumentOperation
76-
*/
77-
this.isDocumentOperation = !!this.targetPosition.root.document;
7868
}
7969

8070
/**

src/model/operation/nooperation.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ import Operation from './operation';
2020
* @extends module:engine/model/operation/operation~Operation
2121
*/
2222
export default class NoOperation extends Operation {
23-
/**
24-
* @inheritDoc
25-
*/
26-
constructor( baseVersion ) {
27-
super( baseVersion );
28-
29-
/**
30-
* @inheritDoc
31-
*/
32-
this.isDocumentOperation = true;
33-
}
34-
3523
get type() {
3624
return 'noop';
3725
}

0 commit comments

Comments
 (0)