diff --git a/packages/ckeditor5-link/src/linkui.js b/packages/ckeditor5-link/src/linkui.js index fcfefc15f6d..843dd190216 100644 --- a/packages/ckeditor5-link/src/linkui.js +++ b/packages/ckeditor5-link/src/linkui.js @@ -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 + } ); + } } } ); } @@ -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; +} diff --git a/packages/ckeditor5-link/tests/linkui.js b/packages/ckeditor5-link/tests/linkui.js index 789ff91cd82..8caa29c18c7 100644 --- a/packages/ckeditor5-link/tests/linkui.js +++ b/packages/ckeditor5-link/tests/linkui.js @@ -477,46 +477,229 @@ describe( 'LinkUI', () => { } ); } ); - it( 'should display a fake visual selection when a text fragment is selected', () => { - setModelData( editor.model, 'f[o]o' ); + describe( 'fake visual selection', () => { + describe( 'non-collapsed', () => { + it( 'should be displayed when a text fragment is selected', () => { + setModelData( editor.model, 'f[o]o' ); - 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( '

f{o}o

' ); - expect( editor.getData() ).to.equal( '

foo

' ); - } ); + expect( getViewData( editor.editing.view ) ).to.equal( '

f{o}o

' ); + expect( editor.getData() ).to.equal( '

foo

' ); + } ); - it( 'should display a fake visual selection on a collapsed selection', () => { - setModelData( editor.model, 'f[]o' ); + 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, '[foo]' ); - 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( + '

[

' + + '

foo]

' + ); + expect( editor.getData() ).to.equal( '

 

foo

' ); + } ); + + 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, 'foo[bar]' ); + + 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( + '

foo{

' + + '

bar]

' + ); + expect( editor.getData() ).to.equal( '

foo

bar

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

foo{

' + + '

' + + '

' + + '

bar

' + + '

' + + '

' + + '

}baz

'; + + expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData ); + expect( editor.getData() ).to.equal( + '

foo

' + + '

 

 

' + + '

bar

' + + '

 

 

' + + '

baz

' + ); + } ); + } ); - expect( getViewData( editor.editing.view ) ).to.equal( - '

f{}o

' - ); - expect( editor.getData() ).to.equal( '

fo

' ); + describe( 'collapsed', () => { + it( 'should be displayed on a collapsed selection', () => { + setModelData( editor.model, 'f[]o' ); + + 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( + '

f{}o

' + ); + expect( editor.getData() ).to.equal( '

fo

' ); + } ); + + 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, 'foo[' + + '' + + ']bar' ); + + 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 = '

foo{

' + + '

' + + '

]bar

'; + + expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData ); + expect( editor.getData() ).to.equal( '

foo

 

bar

' ); + } ); + + 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, 'foo[' + + '' + + '' + + ']bar' ); + + 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 = '

foo{

' + + '

' + + '

' + + '

]bar

'; + + expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData ); + expect( editor.getData() ).to.equal( '

foo

 

 

bar

' ); + } ); + + it( 'should be displayed on selection focus when selection contains few empty elements ' + + '(selection focus is inside an empty element)', () => { + setModelData( editor.model, 'foo[' + + '' + + ']' + + 'bar' ); + + 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 = '

foo{

' + + '

' + + '

]

' + + '

bar

'; + + expect( getViewData( editor.editing.view ) ).to.equal( expectedViewData ); + expect( editor.getData() ).to.equal( '

foo

 

 

bar

' ); + } ); + } ); } ); function getMarkersRange( editor ) {