diff --git a/packages/ckeditor5-link/src/linkui.js b/packages/ckeditor5-link/src/linkui.js index 1e78a65f05e..c24484e39ec 100644 --- a/packages/ckeditor5-link/src/linkui.js +++ b/packages/ckeditor5-link/src/linkui.js @@ -23,6 +23,7 @@ import linkIcon from '../theme/icons/link.svg'; const linkKeystroke = 'Ctrl+K'; const protocolRegExp = /^((\w+:(\/{2,})?)|(\W))/i; const emailRegExp = /[\w-]+@[\w-]+\.+[\w-]+/i; +const VISUAL_SELECTION_MARKER_NAME = 'link-ui'; /** * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the Ctrl+K keystroke. @@ -82,6 +83,23 @@ export default class LinkUI extends Plugin { // Attach lifecycle actions to the the balloon. this._enableUserBalloonInteractions(); + + // Renders a fake visual selection marker on an expanded selection. + editor.conversion.for( 'downcast' ).markerToHighlight( { + model: VISUAL_SELECTION_MARKER_NAME, + view: { + classes: [ 'ck-fake-link-selection' ] + } + } ); + + // Renders a fake visual selection marker on a collapsed selection. + editor.conversion.for( 'downcast' ).markerToElement( { + model: VISUAL_SELECTION_MARKER_NAME, + view: { + name: 'span', + classes: [ 'ck-fake-link-selection', 'ck-fake-link-selection_collapsed' ] + } + } ); } /** @@ -160,7 +178,7 @@ export default class LinkUI extends Plugin { const { value } = formView.urlInputView.fieldView.element; // The regex checks for the protocol syntax ('xxxx://' or 'xxxx:') - // or non-word charecters at the begining of the link ('/', '#' etc.). + // or non-word characters at the beginning of the link ('/', '#' etc.). const isProtocolNeeded = !!defaultProtocol && !protocolRegExp.test( value ); const isEmail = emailRegExp.test( value ); @@ -362,6 +380,8 @@ export default class LinkUI extends Plugin { // Because the form has an input which has focus, the focus must be brought back // to the editor. Otherwise, it would be lost. this.editor.editing.view.focus(); + + this._hideFakeVisualSelection(); } } @@ -382,6 +402,9 @@ export default class LinkUI extends Plugin { } this._addFormView(); + // Show visual selection on a text without a link when the contextual balloon is displayed. + // See https://github.com/ckeditor/ckeditor5/issues/4721. + this._showFakeVisualSelection(); } // If there's a link under the selection... else { @@ -430,6 +453,8 @@ export default class LinkUI extends Plugin { // Then remove the actions view because it's beneath the form. this._balloon.remove( this.actionsView ); + + this._hideFakeVisualSelection(); } /** @@ -609,6 +634,46 @@ export default class LinkUI extends Plugin { } } } + + /** + * Displays a fake visual selection when the contextual balloon is displayed. + * + * This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment. + * + * @private + */ + _showFakeVisualSelection() { + const model = this.editor.model; + + model.change( writer => { + if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) { + writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, { + range: model.document.selection.getFirstRange() + } ); + } else { + writer.addMarker( VISUAL_SELECTION_MARKER_NAME, { + usingOperation: false, + affectsData: false, + range: model.document.selection.getFirstRange() + } ); + } + } ); + } + + /** + * Hides the fake visual selection created in {@link #_showFakeVisualSelection}. + * + * @private + */ + _hideFakeVisualSelection() { + const model = this.editor.model; + + if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) { + model.change( writer => { + writer.removeMarker( VISUAL_SELECTION_MARKER_NAME ); + } ); + } + } } // Returns a link element if there's one among the ancestors of the provided `Position`. diff --git a/packages/ckeditor5-link/tests/linkui.js b/packages/ckeditor5-link/tests/linkui.js index 1f09e43f672..5fad0dcb8d3 100644 --- a/packages/ckeditor5-link/tests/linkui.js +++ b/packages/ckeditor5-link/tests/linkui.js @@ -8,7 +8,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; -import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -351,16 +351,16 @@ describe( 'LinkUI', () => { // https://github.com/ckeditor/ckeditor5-link/issues/113 it( 'updates the position of the panel – creating a new link, then the selection moved', () => { - setModelData( editor.model, 'f[]oo' ); + setModelData( editor.model, 'f[o]o' ); linkUIFeature._showUI(); const spy = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} ); const root = viewDocument.getRoot(); - const text = root.getChild( 0 ).getChild( 0 ); + const text = root.getChild( 0 ).getChild( 2 ); view.change( writer => { - writer.setSelection( text, 3, true ); + writer.setSelection( text, 1, true ); } ); sinon.assert.calledOnce( spy ); @@ -465,6 +465,40 @@ describe( 'LinkUI', () => { sinon.assert.notCalled( spyUpdate ); } ); } ); + + it( 'should display a fake visual selection when a text fragment is selected', () => { + setModelData( editor.model, 'f[o]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, 2 ) + ); + const markerRange = editor.model.markers.get( 'link-ui' ).getRange(); + + expect( markerRange.isEqual( expectedRange ) ).to.be.true; + } ); + + it( 'should display a fake visual selection 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; + } ); } ); describe( '_hideUI()', () => { @@ -518,6 +552,14 @@ describe( 'LinkUI', () => { sinon.assert.notCalled( spy ); } ); + + it( 'should clear the fake visual selection from a selected text fragment', () => { + expect( editor.model.markers.has( 'link-ui' ) ).to.be.true; + + linkUIFeature._hideUI(); + + expect( editor.model.markers.has( 'link-ui' ) ).to.be.false; + } ); } ); describe( 'keyboard support', () => { @@ -1076,6 +1118,16 @@ describe( 'LinkUI', () => { expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com', {} ) ).to.be.true; } ); + it( 'should should clear the fake visual selection on formView#submit event', () => { + linkUIFeature._showUI(); + expect( editor.model.markers.has( 'link-ui' ) ).to.be.true; + + formView.urlInputView.fieldView.value = 'http://cksource.com'; + formView.fire( 'submit' ); + + expect( editor.model.markers.has( 'link-ui' ) ).to.be.false; + } ); + it( 'should hide and reveal the #actionsView on formView#submit event', () => { linkUIFeature._showUI(); formView.fire( 'submit' ); diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-link/link.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-link/link.css index 8a392606461..521ac5924fd 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-link/link.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-link/link.css @@ -7,3 +7,19 @@ .ck .ck-link_selected { background: var(--ck-color-link-selected-background); } + +/* + * Classes used by the "fake visual selection" displayed in the content when an input + * in the link UI has focus (the browser does not render the native selection in this state). + */ +.ck .ck-fake-link-selection { + background: var(--ck-color-link-fake-selection); +} + +/* A collapsed fake visual selection. */ +.ck .ck-fake-link-selection_collapsed { + height: 100%; + border-right: 1px solid var(--ck-color-base-text); + margin-right: -1px; + outline: solid 1px hsla(0, 0%, 100%, .5); +} diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css index 6e6239327db..7a00b5cffbe 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css @@ -106,5 +106,6 @@ /* -- Link -------------------------------------------------------------------------------- */ --ck-color-link-default: hsl(240, 100%, 47%); - --ck-color-link-selected-background: hsla(201, 100%, 56%, 0.1); + --ck-color-link-selected-background: hsla(201, 100%, 56%, 0.1); + --ck-color-link-fake-selection: hsla(201, 100%, 56%, 0.3); } diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 5b19ef07d59..6f9b3c1c2a2 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -132,7 +132,7 @@ animation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards; /* - * The semit-transparent-outline+background combo improves the contrast + * The semi-transparent-outline+background combo improves the contrast * when the background underneath the fake caret is dark. */ outline: solid 1px hsla(0, 0%, 100%, .5);