Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4721: Fake visual selection for opened link UI #7436

Merged
merged 17 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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