Skip to content

Commit

Permalink
Merge pull request #8074 from ckeditor/i/7907
Browse files Browse the repository at this point in the history
Fix (link): Prevented throwing an error during creating a link from the multi-block selection. Closes #7907.
  • Loading branch information
pomek authored Sep 16, 2020
2 parents 1341b3a + 4d20866 commit eb92cfb
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 37 deletions.
51 changes: 43 additions & 8 deletions packages/ckeditor5-link/src/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,16 +662,27 @@ export default class LinkUI extends Plugin {
const model = this.editor.model;

model.change( writer => {
const range = model.document.selection.getFirstRange();

if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, {
range: model.document.selection.getFirstRange()
} );
writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, { range } );
} else {
writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
usingOperation: false,
affectsData: false,
range: model.document.selection.getFirstRange()
} );
if ( range.start.isAtEnd ) {
const focus = model.document.selection.focus;
const nextValidRange = getNextValidRange( range, focus, writer );

writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
usingOperation: false,
affectsData: false,
range: nextValidRange
} );
} else {
writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
usingOperation: false,
affectsData: false,
range
} );
}
}
} );
}
Expand Down Expand Up @@ -700,3 +711,27 @@ export default class LinkUI extends Plugin {
function findLinkElementAncestor( position ) {
return position.getAncestors().find( ancestor => isLinkElement( ancestor ) );
}

// Returns next valid range for the fake visual selection marker.
//
// @private
// @param {module:engine/model/range~Range} range Current range.
// @param {module:engine/model/position~Position} focus Selection focus.
// @param {module:engine/model/writer~Writer} writer Writer.
// @returns {module:engine/model/range~Range} New valid range for the fake visual selection marker.
function getNextValidRange( range, focus, writer ) {
const nextStartPath = [ range.start.path[ 0 ] + 1, 0 ];
const nextStartPosition = writer.createPositionFromPath( range.start.root, nextStartPath, 'toNext' );
const nextRange = writer.createRange( nextStartPosition, range.end );

// Block creating a potential next valid range over the current range end.
if ( nextRange.start.path[ 0 ] > range.end.path[ 0 ] ) {
return writer.createRange( focus );
}

if ( nextStartPosition.isAtStart && nextStartPosition.isAtEnd ) {
return getNextValidRange( nextRange, focus, writer );
}

return nextRange;
}
241 changes: 212 additions & 29 deletions packages/ckeditor5-link/tests/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,46 +477,229 @@ describe( 'LinkUI', () => {
} );
} );

it( 'should display a fake visual selection when a text fragment is selected', () => {
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
describe( 'fake visual selection', () => {
describe( 'non-collapsed', () => {
it( 'should be displayed when a text fragment is selected', () => {
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );

linkUIFeature._showUI();
linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;
expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const paragraph = editor.model.document.getRoot().getChild( 0 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( paragraph, 1 ),
editor.model.createPositionAt( paragraph, 2 )
);
const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
const paragraph = editor.model.document.getRoot().getChild( 0 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( paragraph, 1 ),
editor.model.createPositionAt( paragraph, 2 )
);
const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;
expect( markerRange.isEqual( expectedRange ) ).to.be.true;

expect( getViewData( editor.editing.view ) ).to.equal( '<p>f{<span class="ck-fake-link-selection">o</span>}o</p>' );
expect( editor.getData() ).to.equal( '<p>foo</p>' );
} );
expect( getViewData( editor.editing.view ) ).to.equal( '<p>f{<span class="ck-fake-link-selection">o</span>}o</p>' );
expect( editor.getData() ).to.equal( '<p>foo</p>' );
} );

it( 'should display a fake visual selection on a collapsed selection', () => {
setModelData( editor.model, '<paragraph>f[]o</paragraph>' );
it( 'should display a fake visual selection on the next non-empty text node when selection starts at the end ' +
'of the empty block in the multiline selection', () => {
setModelData( editor.model, '<paragraph>[</paragraph><paragraph>foo]</paragraph>' );

linkUIFeature._showUI();
linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;
expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const secondParagraph = editor.model.document.getRoot().getChild( 1 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( secondParagraph, 0 ),
editor.model.createPositionAt( secondParagraph, 3 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

expect( getViewData( editor.editing.view ) ).to.equal(
'<p>[</p>' +
'<p><span class="ck-fake-link-selection">foo</span>]</p>'
);
expect( editor.getData() ).to.equal( '<p>&nbsp;</p><p>foo</p>' );
} );

it( 'should display a fake visual selection on the next non-empty text node when selection starts at the end ' +
'of the first block in the multiline selection', () => {
setModelData( editor.model, '<paragraph>foo[</paragraph><paragraph>bar]</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const paragraph = editor.model.document.getRoot().getChild( 0 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( paragraph, 1 ),
editor.model.createPositionAt( paragraph, 1 )
);
const markerRange = editor.model.markers.get( 'link-ui' ).getRange();
const secondParagraph = editor.model.document.getRoot().getChild( 1 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( secondParagraph, 0 ),
editor.model.createPositionAt( secondParagraph, 3 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

expect( getViewData( editor.editing.view ) ).to.equal(
'<p>foo{</p>' +
'<p><span class="ck-fake-link-selection">bar</span>]</p>'
);
expect( editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
} );

it( 'should be displayed on first text node in non-empty element when selection contains few empty elements', () => {
setModelData( editor.model, '<paragraph>foo[</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>bar</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>]baz</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

expect( markerRange.isEqual( expectedRange ) ).to.be.true;
const firstNonEmptyElementInTheSelection = editor.model.document.getRoot().getChild( 3 );
const rangeEnd = editor.model.document.selection.getFirstRange().end;
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( firstNonEmptyElementInTheSelection, 0 ),
editor.model.createPositionAt( rangeEnd, 0 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

const expectedViewData = '<p>foo{</p>' +
'<p></p>' +
'<p></p>' +
'<p><span class="ck-fake-link-selection">bar</span></p>' +
'<p></p>' +
'<p></p>' +
'<p>}baz</p>';

expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData );
expect( editor.getData() ).to.equal(
'<p>foo</p>' +
'<p>&nbsp;</p><p>&nbsp;</p>' +
'<p>bar</p>' +
'<p>&nbsp;</p><p>&nbsp;</p>' +
'<p>baz</p>'
);
} );
} );

expect( getViewData( editor.editing.view ) ).to.equal(
'<p>f{}<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span>o</p>'
);
expect( editor.getData() ).to.equal( '<p>fo</p>' );
describe( 'collapsed', () => {
it( 'should be displayed on a collapsed selection', () => {
setModelData( editor.model, '<paragraph>f[]o</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const paragraph = editor.model.document.getRoot().getChild( 0 );
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( paragraph, 1 ),
editor.model.createPositionAt( paragraph, 1 )
);
const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

expect( getViewData( editor.editing.view ) ).to.equal(
'<p>f{}<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span>o</p>'
);
expect( editor.getData() ).to.equal( '<p>fo</p>' );
} );

it( 'should be displayed on selection focus when selection contains only one empty element ' +
'(selection focus is at the beginning of the first non-empty element)', () => {
setModelData( editor.model, '<paragraph>foo[</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>]bar</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const focus = editor.model.document.selection.focus;
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( focus, 0 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

const expectedViewData = '<p>foo{</p>' +
'<p></p>' +
'<p>]<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span>bar</p>';

expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData );
expect( editor.getData() ).to.equal( '<p>foo</p><p>&nbsp;</p><p>bar</p>' );
} );

it( 'should be displayed on selection focus when selection contains few empty elements ' +
'(selection focus is at the beginning of the first non-empty element)', () => {
setModelData( editor.model, '<paragraph>foo[</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>]bar</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const focus = editor.model.document.selection.focus;
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( focus, 0 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

const expectedViewData = '<p>foo{</p>' +
'<p></p>' +
'<p></p>' +
'<p>]<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span>bar</p>';

expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData );
expect( editor.getData() ).to.equal( '<p>foo</p><p>&nbsp;</p><p>&nbsp;</p><p>bar</p>' );
} );

it( 'should be displayed on selection focus when selection contains few empty elements ' +
'(selection focus is inside an empty element)', () => {
setModelData( editor.model, '<paragraph>foo[</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>]</paragraph>' +
'<paragraph>bar</paragraph>' );

linkUIFeature._showUI();

expect( editor.model.markers.has( 'link-ui' ) ).to.be.true;

const focus = editor.model.document.selection.focus;
const expectedRange = editor.model.createRange(
editor.model.createPositionAt( focus, 0 )
);

const markerRange = editor.model.markers.get( 'link-ui' ).getRange();

expect( markerRange.isEqual( expectedRange ) ).to.be.true;

const expectedViewData = '<p>foo{</p>' +
'<p></p>' +
'<p>]<span class="ck-fake-link-selection ck-fake-link-selection_collapsed"></span></p>' +
'<p>bar</p>';

expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData );
expect( editor.getData() ).to.equal( '<p>foo</p><p>&nbsp;</p><p>&nbsp;</p><p>bar</p>' );
} );
} );
} );

function getMarkersRange( editor ) {
Expand Down

0 comments on commit eb92cfb

Please sign in to comment.