Skip to content

Commit

Permalink
Merge pull request #10656 from ckeditor/ck/10628-selection-postfixer-…
Browse files Browse the repository at this point in the history
…merge-ranges

Fix (engine): Merge intersecting ranges that aren't adjacent to each other on ranges array. Closes #10628.
  • Loading branch information
arkflpc authored Nov 16, 2021
2 parents 234b6d3 + b7b6bc8 commit 92565ab
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 28 deletions.
59 changes: 33 additions & 26 deletions packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,35 +263,42 @@ function checkSelectionOnNonLimitElements( start, end, schema ) {
return startIsOnBlock || endIsOnBlock;
}

// Returns a minimal non-intersecting array of ranges.
//
// @param {Array.<module:engine/model/range~Range>} ranges
// @returns {Array.<module:engine/model/range~Range>}
function mergeIntersectingRanges( ranges ) {
const nonIntersectingRanges = [];

// First range will always be fine.
nonIntersectingRanges.push( ranges.shift() );

for ( const range of ranges ) {
const previousRange = nonIntersectingRanges.pop();

if ( range.isEqual( previousRange ) ) {
// Use only one of two identical ranges.
nonIntersectingRanges.push( previousRange );
} else if ( range.isIntersecting( previousRange ) ) {
// Get the sum of two ranges.
const start = previousRange.start.isAfter( range.start ) ? range.start : previousRange.start;
const end = previousRange.end.isAfter( range.end ) ? previousRange.end : range.end;

const merged = new Range( start, end );
nonIntersectingRanges.push( merged );
} else {
nonIntersectingRanges.push( previousRange );
nonIntersectingRanges.push( range );
/**
* Returns a minimal non-intersecting array of ranges without duplicates.
*
* @param {Array.<module:engine/model/range~Range>} Ranges to merge.
* @returns {Array.<module:engine/model/range~Range>} Array of unique and nonIntersecting ranges.
*/
export function mergeIntersectingRanges( ranges ) {
const rangesToMerge = [ ...ranges ];
const rangeIndexesToRemove = new Set();
let currentRangeIndex = 1;

while ( currentRangeIndex < rangesToMerge.length ) {
const currentRange = rangesToMerge[ currentRangeIndex ];
const previousRanges = rangesToMerge.slice( 0, currentRangeIndex );

for ( const [ previousRangeIndex, previousRange ] of previousRanges.entries() ) {
if ( rangeIndexesToRemove.has( previousRangeIndex ) ) {
continue;
}

if ( currentRange.isEqual( previousRange ) ) {
rangeIndexesToRemove.add( previousRangeIndex );
} else if ( currentRange.isIntersecting( previousRange ) ) {
rangeIndexesToRemove.add( previousRangeIndex );
rangeIndexesToRemove.add( currentRangeIndex );

const mergedRange = currentRange.getJoined( previousRange );
rangesToMerge.push( mergedRange );
}
}

currentRangeIndex++;
}

const nonIntersectingRanges = rangesToMerge.filter( ( _, index ) => !rangeIndexesToRemove.has( index ) );

return nonIntersectingRanges;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import Model from '../../../src/model/model';

import { injectSelectionPostFixer } from '../../../src/model/utils/selection-post-fixer';
import { stringify, getData as getModelData, setData as setModelData } from '../../../src/dev-utils/model';
import { injectSelectionPostFixer, mergeIntersectingRanges } from '../../../src/model/utils/selection-post-fixer';

import { getData as getModelData, setData as setModelData } from '../../../src/dev-utils/model';
import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';

describe( 'Selection post-fixer', () => {
Expand Down Expand Up @@ -1098,6 +1098,86 @@ describe( 'Selection post-fixer', () => {
} );
} );

describe( 'intersecting selection - merge ranges', () => {
it( 'should return one merged range: A+B+C - #1', () => {
setModelData( model,
'<paragraph>fo[o b][ar b]az</paragraph>'
);

const rangeA = model.document.selection.getFirstRange();
const rangeB = model.document.selection.getLastRange();
// A range containing both rangeA and rangeB.
const rangeC = model.createRange(
model.createPositionAt( modelRoot.getChild( 0 ), 0 ),
model.createPositionAt( modelRoot.getChild( 0 ), 11 )
);

const mergedRanges = mergeIntersectingRanges( [ rangeA, rangeB, rangeC ] );

expect( mergedRanges.length ).to.equal( 1 );
expect( stringify( model.document.getRoot(), mergedRanges[ 0 ] ) ).to.equal( '<paragraph>[foo bar baz]</paragraph>' );
} );

it( 'should return one merged range: A+B+C - #2', () => {
setModelData( model,
'<paragraph>f[oo b]a[r ba]z</paragraph>'
);

const rangeA = model.document.selection.getFirstRange();
const rangeB = model.document.selection.getLastRange();
// Range intersecting with rangeA and rangeB
const rangeC = model.createRange(
model.createPositionAt( modelRoot.getChild( 0 ), 4 ),
model.createPositionAt( modelRoot.getChild( 0 ), 11 )
);

const mergedRanges = mergeIntersectingRanges( [ rangeA, rangeB, rangeC ] );

expect( mergedRanges.length ).to.equal( 1 );
expect( stringify( model.document.getRoot(), mergedRanges[ 0 ] ) ).to.equal( '<paragraph>f[oo bar baz]</paragraph>' );
} );

it( 'should return two ranges: A, B+C', () => {
setModelData( model,
'<paragraph>f[oo] ba[r ba]z</paragraph>'
);

const rangeA = model.document.selection.getFirstRange();
const rangeB = model.document.selection.getLastRange();
// Range intersecting with rangeB
const rangeC = model.createRange(
model.createPositionAt( modelRoot.getChild( 0 ), 4 ),
model.createPositionAt( modelRoot.getChild( 0 ), 10 )
);

const mergedRanges = mergeIntersectingRanges( [ rangeA, rangeB, rangeC ] );

expect( mergedRanges.length ).to.equal( 2 );
expect( stringify( model.document.getRoot(), mergedRanges[ 0 ] ) ).to.equal( '<paragraph>f[oo] bar baz</paragraph>' );
expect( stringify( model.document.getRoot(), mergedRanges[ 1 ] ) ).to.equal( '<paragraph>foo [bar ba]z</paragraph>' );
} );

it( 'should return two ranges: A+B,C', () => {
setModelData( model,
'<paragraph>f[oo][ bar] baz</paragraph>' // foo bar baz
);

const rangeA = model.document.selection.getFirstRange();
const rangeB = model.document.selection.getLastRange();
const rangeC = model.createRange(
model.createPositionAt( modelRoot.getChild( 0 ), 8 ),
model.createPositionAt( modelRoot.getChild( 0 ), 11 )
);

const mergedRanges = mergeIntersectingRanges( [ rangeA, rangeB, rangeC ] );

expect( mergedRanges.length ).to.equal( 3 );
expect( stringify( model.document.getRoot(), mergedRanges[ 0 ] ) ).to.equal( '<paragraph>f[oo] bar baz</paragraph>' );
expect( stringify( model.document.getRoot(), mergedRanges[ 1 ] ) ).to.equal( '<paragraph>foo[ bar] baz</paragraph>' );
expect( stringify( model.document.getRoot(), mergedRanges[ 2 ] ) ).to.equal( '<paragraph>foo bar [baz]</paragraph>' );
} );
} );

describe( 'non-collapsed selection - other scenarios', () => {
it( 'should fix #1 (element selection of not an object)', () => {
setModelData( model,
Expand Down

0 comments on commit 92565ab

Please sign in to comment.