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

Commit b71adfb

Browse files
authored
Merge pull request #1113 from ckeditor/t/1112
Fix: Fixed incorrect markers transformations and conversions. Closes #1112. Closes #1080. Closes #1079.
2 parents 885901f + 267cbdf commit b71adfb

File tree

4 files changed

+205
-9
lines changed

4 files changed

+205
-9
lines changed

src/conversion/modelconversiondispatcher.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,11 @@ export default class ModelConversionDispatcher {
217217

218218
for ( const marker of this._modelDocument.markers ) {
219219
const markerRange = marker.getRange();
220+
const intersection = markerRange.getIntersection( range );
220221

221222
// Check if inserted content is inserted into a marker.
222-
if ( markerRange.containsPosition( range.start ) ) {
223-
this.convertMarker( 'addMarker', marker.name, markerRange.getIntersection( range ) );
224-
}
225-
226-
// Check if inserted content contains a marker.
227-
if ( range.containsRange( markerRange, true ) ) {
228-
this.convertMarker( 'addMarker', marker.name, markerRange );
223+
if ( intersection && shouldMarkerChangeBeConverted( range.start, marker, this.conversionApi.mapper ) ) {
224+
this.convertMarker( 'addMarker', marker.name, intersection );
229225
}
230226
}
231227
}
@@ -367,10 +363,16 @@ export default class ModelConversionDispatcher {
367363
this.fire( 'selection', { selection }, consumable, this.conversionApi );
368364

369365
for ( const marker of markers ) {
366+
const markerRange = marker.getRange();
367+
368+
if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
369+
continue;
370+
}
371+
370372
const data = {
371373
selection,
372374
markerName: marker.name,
373-
markerRange: marker.getRange()
375+
markerRange
374376
};
375377

376378
if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
@@ -717,3 +719,27 @@ export default class ModelConversionDispatcher {
717719
}
718720

719721
mix( ModelConversionDispatcher, EmitterMixin );
722+
723+
// Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
724+
// converted if they happen inside an element with custom conversion method.
725+
//
726+
// @param {module:engine/model/position~Position} modelPosition
727+
// @param {module:engine/model/markercollection~Marker} marker
728+
// @param {module:engine/conversion/mapper~Mapper} mapper
729+
// @returns {Boolean}
730+
function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
731+
const range = marker.getRange();
732+
const ancestors = Array.from( modelPosition.getAncestors() );
733+
ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
734+
ancestors.reverse();
735+
736+
const hasCustomHandling = ancestors.some( element => {
737+
if ( range.containsItem( element ) ) {
738+
const viewElement = mapper.toViewElement( element );
739+
740+
return !!viewElement.getCustomProperty( 'addHighlight' );
741+
}
742+
} );
743+
744+
return !hasCustomHandling;
745+
}

src/model/range.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ export default class Range {
117117
return containsStart && containsEnd;
118118
}
119119

120+
/**
121+
* Checks whether given {@link module:engine/model/item~Item} is inside this range.
122+
*
123+
* @param {module:engine/model/item~Item} item Model item to check.
124+
*/
125+
containsItem( item ) {
126+
const pos = Position.createBefore( item );
127+
128+
return this.containsPosition( pos ) || this.start.isEqual( pos );
129+
}
130+
120131
/**
121132
* Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
122133
*
@@ -494,7 +505,7 @@ export default class Range {
494505
// ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p> --> <p>a[b</p><p>xx</p><w></w><p>c]d</p> // Note <p>xx</p> inclusion.
495506
// <w>{<p>a[b</p>}</w>^<p>c]d</p> --> <w></w><p>a[b</p><p>c]d</p>
496507
if (
497-
sourceRange.containsPosition( this.start ) &&
508+
( sourceRange.containsPosition( this.start ) || sourceRange.start.isEqual( this.start ) ) &&
498509
this.containsPosition( sourceRange.end ) &&
499510
this.end.isAfter( targetPosition )
500511
) {

tests/conversion/modelconversiondispatcher.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import RenameOperation from '../../src/model/operation/renameoperation';
1515
import AttributeOperation from '../../src/model/operation/attributeoperation';
1616
import { wrapInDelta } from '../../tests/model/_utils/utils';
1717

18+
import ViewContainerElement from '../../src/view/containerelement';
19+
1820
describe( 'ModelConversionDispatcher', () => {
1921
let dispatcher, doc, root, gyPos;
2022

@@ -360,6 +362,78 @@ describe( 'ModelConversionDispatcher', () => {
360362
expect( callArgs[ 1 ] ).to.equal( 'marker' );
361363
expect( callArgs[ 2 ].isEqual( markerRange ) ).to.be.true;
362364
} );
365+
366+
it( 'should not fire marker conversion if content is inserted into element with custom highlight handling', () => {
367+
sinon.spy( dispatcher, 'convertMarker' );
368+
369+
const text = new ModelText( 'abc' );
370+
const caption = new ModelElement( 'caption', null, text );
371+
const image = new ModelElement( 'image', null, caption );
372+
root.appendChildren( [ image ] );
373+
374+
// Create view elements that will be "mapped" to model elements.
375+
const viewCaption = new ViewContainerElement( 'caption' );
376+
const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
377+
378+
// Create custom highlight handler mock.
379+
viewFigure.setCustomProperty( 'addHighlight', () => {} );
380+
viewFigure.setCustomProperty( 'removeHighlight', () => {} );
381+
382+
// Create mapper mock.
383+
dispatcher.conversionApi.mapper = {
384+
toViewElement( modelElement ) {
385+
if ( modelElement == image ) {
386+
return viewFigure;
387+
} else if ( modelElement == caption ) {
388+
return viewCaption;
389+
}
390+
}
391+
};
392+
393+
const markerRange = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
394+
doc.markers.set( 'marker', markerRange );
395+
396+
const insertionRange = ModelRange.createFromParentsAndOffsets( caption, 1, caption, 2 );
397+
dispatcher.convertInsertion( insertionRange );
398+
399+
expect( dispatcher.convertMarker.called ).to.be.false;
400+
} );
401+
402+
it( 'should fire marker conversion if inserted into element with highlight handling but element is not in marker range', () => {
403+
sinon.spy( dispatcher, 'convertMarker' );
404+
405+
const text = new ModelText( 'abc' );
406+
const caption = new ModelElement( 'caption', null, text );
407+
const image = new ModelElement( 'image', null, caption );
408+
root.appendChildren( [ image ] );
409+
410+
// Create view elements that will be "mapped" to model elements.
411+
const viewCaption = new ViewContainerElement( 'caption' );
412+
const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
413+
414+
// Create custom highlight handler mock.
415+
viewFigure.setCustomProperty( 'addHighlight', () => {} );
416+
viewFigure.setCustomProperty( 'removeHighlight', () => {} );
417+
418+
// Create mapper mock.
419+
dispatcher.conversionApi.mapper = {
420+
toViewElement( modelElement ) {
421+
if ( modelElement == image ) {
422+
return viewFigure;
423+
} else if ( modelElement == caption ) {
424+
return viewCaption;
425+
}
426+
}
427+
};
428+
429+
const markerRange = ModelRange.createFromParentsAndOffsets( caption, 0, caption, 3 );
430+
doc.markers.set( 'marker', markerRange );
431+
432+
const insertionRange = ModelRange.createFromParentsAndOffsets( caption, 2, caption, 3 );
433+
dispatcher.convertInsertion( insertionRange );
434+
435+
expect( dispatcher.convertMarker.called ).to.be.true;
436+
} );
363437
} );
364438

365439
describe( 'convertMove', () => {
@@ -600,6 +674,46 @@ describe( 'ModelConversionDispatcher', () => {
600674
expect( dispatcher.fire.calledWith( 'selectionMarker:name' ) ).to.be.true;
601675
} );
602676

677+
it( 'should not fire event for marker if selection is in a element with custom highlight handling', () => {
678+
// Clear after `beforeEach`.
679+
root.removeChildren( 0, root.childCount );
680+
681+
const text = new ModelText( 'abc' );
682+
const caption = new ModelElement( 'caption', null, text );
683+
const image = new ModelElement( 'image', null, caption );
684+
root.appendChildren( [ image ] );
685+
686+
// Create view elements that will be "mapped" to model elements.
687+
const viewCaption = new ViewContainerElement( 'caption' );
688+
const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
689+
690+
// Create custom highlight handler mock.
691+
viewFigure.setCustomProperty( 'addHighlight', () => {} );
692+
viewFigure.setCustomProperty( 'removeHighlight', () => {} );
693+
694+
// Create mapper mock.
695+
dispatcher.conversionApi.mapper = {
696+
toViewElement( modelElement ) {
697+
if ( modelElement == image ) {
698+
return viewFigure;
699+
} else if ( modelElement == caption ) {
700+
return viewCaption;
701+
}
702+
}
703+
};
704+
705+
doc.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 1 ) );
706+
doc.selection.setRanges( [ ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) ] );
707+
708+
sinon.spy( dispatcher, 'fire' );
709+
710+
const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
711+
712+
dispatcher.convertSelection( doc.selection, markers );
713+
714+
expect( dispatcher.fire.calledWith( 'selectionMarker:name' ) ).to.be.false;
715+
} );
716+
603717
it( 'should not fire events if information about marker has been consumed', () => {
604718
doc.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
605719
doc.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );

tests/model/range.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,12 @@ describe( 'Range', () => {
412412

413413
expect( range.containsPosition( position ) ).to.be.true;
414414
} );
415+
} );
416+
417+
describe( 'containsRange()', () => {
418+
beforeEach( () => {
419+
range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
420+
} );
415421

416422
it( 'should return true if ranges are equal and check is not strict', () => {
417423
const otherRange = Range.createFromRange( range );
@@ -441,6 +447,32 @@ describe( 'Range', () => {
441447
} );
442448
} );
443449

450+
describe( 'containsItem()', () => {
451+
let a, b, c, d, xxx;
452+
453+
beforeEach( () => {
454+
a = new Element( 'a' );
455+
b = new Element( 'b' );
456+
c = new Element( 'c' );
457+
d = new Element( 'd' );
458+
459+
xxx = new Text( 'xxx' );
460+
b.appendChildren( xxx );
461+
462+
root.appendChildren( [ a, b, c, d ] );
463+
} );
464+
465+
it( 'should return true if element is inside range and false when it is not inside range', () => {
466+
const range = Range.createFromParentsAndOffsets( root, 1, root, 3 ); // Range over `b` and `c`.
467+
468+
expect( range.containsItem( a ) ).to.be.false;
469+
expect( range.containsItem( b ) ).to.be.true;
470+
expect( range.containsItem( xxx ) ).to.be.true;
471+
expect( range.containsItem( c ) ).to.be.true;
472+
expect( range.containsItem( d ) ).to.be.false;
473+
} );
474+
} );
475+
444476
describe( '_getTransformedByInsertion()', () => {
445477
it( 'should return an array of Range objects', () => {
446478
const range = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
@@ -954,6 +986,19 @@ describe( 'Range', () => {
954986
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 1, 1 ] );
955987
} );
956988

989+
it( 'split at the beginning of multi-element range', () => {
990+
range.start = new Position( root, [ 0, 4 ] );
991+
range.end = new Position( root, [ 1, 2 ] );
992+
993+
const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 3, 1 );
994+
995+
const transformed = range.getTransformedByDelta( delta );
996+
997+
expect( transformed.length ).to.equal( 1 );
998+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 1, 0 ] );
999+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
1000+
} );
1001+
9571002
it( 'split inside range which starts at the beginning of split element', () => {
9581003
range.start = new Position( root, [ 0, 0 ] );
9591004
range.end = new Position( root, [ 0, 4 ] );

0 commit comments

Comments
 (0)