Ver código fonte

Improved removing content after a link element.

Kamil Piechaczek 5 anos atrás
pai
commit
c9c34998ff

+ 93 - 12
packages/ckeditor5-link/src/linkediting.js

@@ -17,6 +17,7 @@ import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
 import ManualDecorator from './utils/manualdecorator';
 import findAttributeRange from '@ckeditor/ckeditor5-typing/src/utils/findattributerange';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators } from './utils';
 
 import '../theme/link.css';
@@ -116,6 +117,9 @@ export default class LinkEditing extends Plugin {
 
 		// Handle typing over the link.
 		this._enableTypingOverLink();
+
+		// Handle removing the content after the link element.
+		this._enableRemovingLinkAttributesWhileRemovingEndOfLink();
 	}
 
 	/**
@@ -224,7 +228,7 @@ export default class LinkEditing extends Plugin {
 		const model = editor.model;
 		const selection = model.document.selection;
 
-		model.on( 'insertContent', () => {
+		this.listenTo( model, 'insertContent', () => {
 			const nodeBefore = selection.anchor.nodeBefore;
 			const nodeAfter = selection.anchor.nodeAfter;
 
@@ -291,13 +295,8 @@ export default class LinkEditing extends Plugin {
 				return;
 			}
 
-			// Make the selection free of link-related model attributes.
-			// All link-related model attributes start with "link". That includes not only "linkHref"
-			// but also all decorator attributes (they have dynamic names).
 			model.change( writer => {
-				[ ...model.document.selection.getAttributeKeys() ]
-					.filter( name => name.startsWith( 'link' ) )
-					.forEach( name => writer.removeSelectionAttribute( name ) );
+				removeLinkAttributesFromSelection( writer, editor.commands.get( 'link' ) );
 			} );
 		}, { priority: 'low' } );
 	}
@@ -353,11 +352,7 @@ export default class LinkEditing extends Plugin {
 			// 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 );
-					}
+					removeLinkAttributesFromSelection( writer, editor.commands.get( 'link' ) );
 				} );
 			}
 		} );
@@ -438,6 +433,92 @@ export default class LinkEditing extends Plugin {
 			selectionAttributes = null;
 		}, { priority: 'high' } );
 	}
+
+	/**
+	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
+	 * removing a content after a link element.
+	 *
+	 * If so, the selection should not preserve the `linkHref` attribute. However, if
+	 * {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is activated and
+	 * the selection is inside the link, at the end, the `linkHref` attribute should stay untouched.
+	 *
+	 * The purpose of this action is to allow removing the link label and keep the selection outside the link.
+	 *
+	 * See https://github.com/ckeditor/ckeditor5/issues/7521.
+	 *
+	 * @private
+	 */
+	_enableRemovingLinkAttributesWhileRemovingEndOfLink() {
+		const editor = this.editor;
+		const model = editor.model;
+		const selection = model.document.selection;
+		const view = editor.editing.view;
+
+		// A flag whether attributes `linkHref` attribute should be preserved.
+		let shouldPreserveAttributes = false;
+
+		// A flag whether the `Backspace` key was pressed.
+		let hasBackspacePressed = false;
+
+		// Detect pressing `Backspace`.
+		this.listenTo( view.document, 'delete', ( evt, data ) => {
+			hasBackspacePressed = data.domEvent.keyCode === keyCodes.backspace;
+		}, { priority: 'high' } );
+
+		// Before removing the content, check whether the selection is inside a link or at the end of link but with 2-SCM enabled.
+		// If so, we want to preserve link attributes.
+		this.listenTo( model, 'deleteContent', () => {
+			// Reset the state.
+			shouldPreserveAttributes = false;
+
+			const position = selection.getFirstPosition();
+			const linkHref = selection.getAttribute( 'linkHref' );
+
+			if ( !linkHref ) {
+				return;
+			}
+
+			const linkRange = findAttributeRange( position, 'linkHref', linkHref, model );
+
+			// Preserve `linkHref` attribute if the selection is in the middle of the link or
+			// the selection is at the end of the link and 2-SCM is activated.
+			shouldPreserveAttributes = linkRange.containsPosition( position ) || linkRange.end.isEqual( position );
+		}, { priority: 'high' } );
+
+		// After removing the content, check whether the current selection should preserve the `linkHref` attribute.
+		this.listenTo( model, 'deleteContent', () => {
+			// If didn't press `Backspace`.
+			if ( !hasBackspacePressed ) {
+				return;
+			}
+
+			hasBackspacePressed = false;
+
+			// Disable the mechanism if inside a link (`<$text url="foo">F[]oo</$text>` or <$text url="foo">Foo[]</$text>`).
+			if ( shouldPreserveAttributes ) {
+				return;
+			}
+
+			// Use `model.enqueueChange()` in order to execute the callback at the end of the changes process.
+			editor.model.enqueueChange( writer => {
+				removeLinkAttributesFromSelection( writer, editor.commands.get( 'link' ) );
+			} );
+		}, { priority: 'low' } );
+	}
+}
+
+// Make the selection free of link-related model attributes.
+// All link-related model attributes start with "link". That includes not only "linkHref"
+// but also all decorator attributes (they have dynamic names).
+//
+// @param {module:engine/model/writer~Writer} writer
+// @param {module:link/linkcommand~LinkCommand} linkCommand
+function removeLinkAttributesFromSelection( writer, linkCommand ) {
+	writer.removeSelectionAttribute( 'linkHref' );
+
+	for ( const manualDecorator of linkCommand.manualDecorators ) {
+		writer.removeSelectionAttribute( manualDecorator.id );
+	}
 }
 
 // Checks whether selection's attributes should be copied to the new inserted text.

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

@@ -17,6 +17,7 @@ import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventd
 import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Input from '@ckeditor/ckeditor5-typing/src/input';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
 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 { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
@@ -1406,4 +1407,139 @@ describe( 'LinkEditing', () => {
 			};
 		}
 	} );
+
+	// https://github.com/ckeditor/ckeditor5/issues/7521
+	describe( 'removing a character before the link element', () => {
+		let editor;
+
+		beforeEach( async () => {
+			editor = await ClassicTestEditor.create( element, {
+				plugins: [ Paragraph, LinkEditing, Delete, 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;
+
+			model.schema.extend( '$text', {
+				allowIn: '$root',
+				allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
+			} );
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		it( 'should not preserve the `linkHref` attribute when deleting content after the link', () => {
+			setModelData( model, '<paragraph>Foo <$text linkHref="url">Bar</$text> []</paragraph>' );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'initial state' ).to.equal( false );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing space after the link' ).to.equal( false );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing a character in the link' ).to.equal( false );
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo <$text linkHref="url">Ba</$text>[]</paragraph>' );
+		} );
+
+		it( 'should preserve the `linkHref` attribute when deleting content while the selection is at the end of the link', () => {
+			setModelData( model, '<paragraph>Foo <$text linkHref="url">Bar []</$text></paragraph>' );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'initial state' ).to.equal( true );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing space after the link' ).to.equal( true );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing a character in the link' ).to.equal( true );
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo <$text linkHref="url">Ba[]</$text></paragraph>' );
+		} );
+
+		it( 'should preserve the `linkHref` attribute when deleting content while the selection is inside the link', () => {
+			setModelData( model, '<paragraph>Foo <$text linkHref="url">A long URLLs[] description</$text></paragraph>' );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'initial state' ).to.equal( true );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing space after the link' ).to.equal( true );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing a character in the link' ).to.equal( true );
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo <$text linkHref="url">A long URL[] description</$text></paragraph>' );
+		} );
+
+		it( 'should do nothing if there is no `linkHref` attribute', () => {
+			setModelData( model, '<paragraph>Foo <$text bold="true">Bolded.</$text> []Bar</paragraph>' );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.backspace,
+				preventDefault: () => {}
+			} ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo <$text bold="true">Bolded[]</$text>Bar</paragraph>' );
+		} );
+
+		it( 'should preserve the `linkHref` attribute when deleting content using "Delete" key', () => {
+			setModelData( model, '<paragraph>Foo <$text linkHref="url">Bar</$text>[ ]</paragraph>' );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'initial state' ).to.equal( false );
+
+			view.document.fire( 'delete', new DomEventData( view.document, {
+				keyCode: keyCodes.delete,
+				preventDefault: () => {}
+			}, { direction: 'forward' } ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo <$text linkHref="url">Bar[]</$text></paragraph>' );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing space after the link' ).to.equal( true );
+		} );
+	} );
 } );