Skip to content

Commit

Permalink
Merge pull request #7436 from ckeditor/i/4721
Browse files Browse the repository at this point in the history
Feature (link): A fake caret (selection) should be displayed in the content when the link input has focus and the browser does not render the native caret (selection). Closes #4721.

Feature (theme-lark): Added styles for the fake link caret (selection) (see #4721).
  • Loading branch information
oleq authored Jun 16, 2020
2 parents 61a6110 + ace6bbe commit ffac139
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 7 deletions.
67 changes: 66 additions & 1 deletion packages/ckeditor5-link/src/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Ctrl+K</kbd> keystroke.
Expand Down Expand Up @@ -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' ]
}
} );
}

/**
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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`.
Expand Down
60 changes: 56 additions & 4 deletions packages/ckeditor5-link/tests/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, '<paragraph>f[]oo</paragraph>' );
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );

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 );
Expand Down Expand Up @@ -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, '<paragraph>f[o]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, 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, '<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;
} );
} );

describe( '_hideUI()', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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' );
Expand Down
16 changes: 16 additions & 0 deletions packages/ckeditor5-theme-lark/theme/ckeditor5-link/link.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit ffac139

Please sign in to comment.