Explorar el Código

After clicking a link, the selection will be moved before/after if clicked in a boundary.

Kamil Piechaczek hace 5 años
padre
commit
37c744a7fa

+ 68 - 2
packages/ckeditor5-link/src/linkediting.js

@@ -8,13 +8,15 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
+import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
 import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
-import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators } from './utils';
 import AutomaticDecorators from './utils/automaticdecorators';
 import ManualDecorator from './utils/manualdecorator';
-import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
 import findLinkRange from './findlinkrange';
+import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators } from './utils';
+
 import '../theme/link.css';
 
 const HIGHLIGHT_CLASS = 'ck-link_selected';
@@ -104,6 +106,9 @@ export default class LinkEditing extends Plugin {
 
 		// Change the attributes of the selection in certain situations after the link was inserted into the document.
 		this._enableInsertContentSelectionAttributesFixer();
+
+		// Handle a click at the beginning/end of a link element.
+		this._enableClickingAfterLink();
 	}
 
 	/**
@@ -347,4 +352,65 @@ export default class LinkEditing extends Plugin {
 			} );
 		}, { priority: 'low' } );
 	}
+
+	/**
+	 * Starts listening to {@link module:engine/view/document~Document#event:mousedown} and
+	 * {@link module:engine/view/document~Document#event:selectionChange} and puts the selection before/after a link node
+	 * if clicked at the beginning/ending of the link.
+	 *
+	 * The purpose of this action is to allow typing around the link node directly after a click.
+	 *
+	 * See https://github.com/ckeditor/ckeditor5/issues/1016.
+	 *
+	 * @private
+	 */
+	_enableClickingAfterLink() {
+		const editor = this.editor;
+
+		editor.editing.view.addObserver( MouseObserver );
+
+		let clicked = false;
+
+		// Detect the click.
+		this.listenTo( editor.editing.view.document, 'mousedown', () => {
+			clicked = true;
+		} );
+
+		// When the selection has changed...
+		this.listenTo( editor.editing.view.document, 'selectionChange', () => {
+			if ( !clicked ) {
+				return;
+			}
+
+			// ...and it was caused by the click...
+			clicked = false;
+
+			const selection = editor.model.document.selection;
+
+			// ...and no text is selected...
+			if ( !selection.isCollapsed ) {
+				return;
+			}
+
+			// ...and clicked text is the link...
+			if ( !selection.hasAttribute( 'linkHref' ) ) {
+				return;
+			}
+
+			const position = selection.getFirstPosition();
+			const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), editor.model );
+
+			// ...check whether clicked start/end boundary of the link.
+			// If so, remove the `linkHref` attribute.
+			if ( position.isTouching( linkRange.start ) || position.isTouching( linkRange.end ) ) {
+				editor.model.change( writer => {
+					writer.removeSelectionAttribute( 'linkHref' );
+
+					for ( const manualDecorator of editor.commands.get( 'link' ).manualDecorators ) {
+						writer.removeSelectionAttribute( manualDecorator.id );
+					}
+				} );
+			}
+		} );
+	}
 }

+ 196 - 0
packages/ckeditor5-link/tests/linkediting.js

@@ -15,6 +15,8 @@ import { getData as getModelData, setData as setModelData } from '@ckeditor/cked
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { isLinkElement } from '../src/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
 
 /* global document */
 
@@ -851,4 +853,198 @@ describe( 'LinkEditing', () => {
 			} );
 		} );
 	} );
+
+	// https://github.com/ckeditor/ckeditor5/issues/1016
+	describe( 'typing around the link after a click', () => {
+		let editor;
+
+		beforeEach( async () => {
+			editor = await ClassicTestEditor.create( element, {
+				plugins: [ Paragraph, LinkEditing, Enter, Typing, BoldEditing ],
+				link: {
+					decorators: {
+						isFoo: {
+							mode: 'manual',
+							label: 'Foo',
+							attributes: {
+								class: 'foo'
+							}
+						},
+						isBar: {
+							mode: 'manual',
+							label: 'Bar',
+							attributes: {
+								target: '_blank'
+							}
+						}
+					}
+				}
+			} );
+
+			model = editor.model;
+			view = editor.editing.view;
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		it( 'should insert content after the link', () => {
+			setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>[]</paragraph>' );
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>Foo[]</paragraph>' );
+		} );
+
+		it( 'should insert content before the link', () => {
+			setModelData( model, '<paragraph><$text linkHref="url">[]Bar</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>[]<$text linkHref="url">Bar</$text></paragraph>' );
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo[]<$text linkHref="url">Bar</$text></paragraph>' );
+		} );
+
+		it( 'should insert content to the link if clicked inside it', () => {
+			setModelData( model, '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
+
+			editor.execute( 'input', { text: 'ar. B' } );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar. B[]ar</$text></paragraph>' );
+		} );
+
+		it( 'should insert content between two links (selection at the end of the first link)', () => {
+			setModelData( model, '<paragraph><$text linkHref="foo">Foo[]</$text><$text linkHref="bar">Bar</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
+			);
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
+			);
+		} );
+
+		it( 'should insert content between two links (selection at the beginning of the second link)', () => {
+			setModelData( model, '<paragraph><$text linkHref="foo">Foo</$text><$text linkHref="bar">[]Bar</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
+			);
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
+			);
+		} );
+
+		it( 'should not touch other attributes than `linkHref`', () => {
+			setModelData( model, '<paragraph><$text bold="true" linkHref="url">Bar[]</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">[]</$text></paragraph>'
+			);
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">Foo[]</$text></paragraph>'
+			);
+		} );
+
+		it( 'should do nothing if the text was not clicked', () => {
+			setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
+		} );
+
+		it( 'should do nothing if the selection is not collapsed after the click', () => {
+			setModelData( model, '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
+		} );
+
+		it( 'should do nothing if the text is not a link', () => {
+			setModelData( model, '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
+		} );
+
+		it( 'should remove manual decorators', () => {
+			model.schema.extend( '$text', {
+				allowIn: '$root',
+				allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
+			} );
+
+			setModelData( model, '<paragraph><$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]</$text></paragraph>' );
+
+			editor.editing.view.document.fire( 'mousedown' );
+			editor.editing.view.document.fire( 'selectionChange', {
+				newSelection: view.document.selection
+			} );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>[]</paragraph>'
+			);
+
+			editor.execute( 'input', { text: 'Foo' } );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>Foo[]</paragraph>'
+			);
+		} );
+	} );
 } );