Procházet zdrojové kódy

Merge pull request #7436 from ckeditor/i/4721

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).
Aleksander Nowodzinski před 5 roky
rodič
revize
ffac139a3e

+ 66 - 1
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 <kbd>Ctrl+K</kbd> 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`.

+ 56 - 4
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, '<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 );
@@ -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()', () => {
@@ -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' );

+ 16 - 0
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);
+}

+ 2 - 1
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);
 }

+ 1 - 1
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);