Selaa lähdekoodia

Implemented a WidgetTypeAround integration with the delete/backspace.

Aleksander Nowodzinski 5 vuotta sitten
vanhempi
commit
bc9a93e2d0

+ 75 - 2
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -77,6 +77,7 @@ export default class WidgetTypeAround extends Plugin {
 		this._enableInsertingParagraphsOnEnterKeypress();
 		this._enableInsertingParagraphsOnUnsafeKeystroke();
 		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
+		this._enableDeleteIntegration();
 	}
 
 	/**
@@ -474,15 +475,87 @@ export default class WidgetTypeAround extends Plugin {
 	_enableInsertingParagraphsOnUnsafeKeystroke() {
 		const editor = this.editor;
 		const editingView = editor.editing.view;
+		const keyCodesHandledSomewhereElse = [
+			keyCodes.enter,
+			keyCodes.delete,
+			keyCodes.backspace
+		];
 
 		// Note: The priority must precede the default Widget class keydown handler.
 		editingView.document.on( 'keydown', ( evt, domEventData ) => {
-			// Don't handle enter here. It's handled in a separate listener.
-			if ( domEventData.keyCode !== keyCodes.enter && !isSafeKeystroke( domEventData ) ) {
+			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
+			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isSafeKeystroke( domEventData ) ) {
 				this._insertParagraphAccordingToSelectionAttribute();
 			}
 		}, { priority: priorities.get( 'high' ) + 1 } );
 	}
+
+	/**
+	 * It creates a "delete" event listener on the view document to handle cases when delete/backspace
+	 * is pressed and the "fake caret" is currently active.
+	 *
+	 * The "fake caret" should create an illusion of a "real browser caret" so that when it appears
+	 * before/after a widget, pressing delete/backspace should remove a widget or delete a content
+	 * before/after a widget (depending on the content surrounding the widget).
+	 *
+	 * @private
+	 */
+	_enableDeleteIntegration() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const model = editor.model;
+		const schema = model.schema;
+
+		// Note: The priority must precede the default Widget class delete handler.
+		this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => {
+			const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			// This listener handles only these cases when the "fake caret" is active.
+			if ( !typeAroundSelectionAttributeValue ) {
+				return false;
+			}
+
+			const direction = domEventData.direction;
+			const selectedModelWidget = model.document.selection.getSelectedElement();
+
+			if ( typeAroundSelectionAttributeValue === 'before' ) {
+				if ( direction === 'backward' ) {
+					const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction );
+
+					if ( range ) {
+						model.change( writer => {
+							writer.setSelection( range );
+							editor.execute( 'delete' );
+						} );
+					}
+				} else {
+					editor.execute( 'delete', {
+						selection: model.createSelection( selectedModelWidget, 'on' )
+					} );
+				}
+			} else {
+				if ( direction === 'backward' ) {
+					editor.execute( 'delete', {
+						selection: model.createSelection( selectedModelWidget, 'on' )
+					} );
+				} else {
+					const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction );
+
+					if ( range ) {
+						model.change( writer => {
+							writer.setSelection( range );
+							editor.execute( 'forwardDelete' );
+						} );
+					}
+				}
+			}
+
+			// If some content was deleted, don't let the handler from the Widget plugin kick in.
+			// If nothing was deleted, then the default handler will have nothing to do anyway.
+			domEventData.preventDefault();
+			evt.stop();
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
 }
 
 // Injects the type around UI into a view widget instance.

+ 188 - 0
packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js

@@ -875,6 +875,194 @@ describe( 'WidgetTypeAround', () => {
 			} );
 		} );
 
+		describe( 'delete integration', () => {
+			let eventInfoStub, domEventDataStub;
+
+			describe( 'backward delete', () => {
+				it( 'should delete content before a widget if the "fake caret" is also before the widget', () => {
+					setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowleft' );
+
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					fireDeleteEvent();
+					expect( getModelData( model ) ).to.equal( '<paragraph>fo[]</paragraph><blockWidget></blockWidget>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should do nothing if the "fake caret" is before the widget but there is nothing to delete there', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowleft' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					fireDeleteEvent();
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a widget if the "fake caret" is after the widget (no content after the widget)', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowright' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					fireDeleteEvent();
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a widget if the "fake caret" is after the widget (some content after the widget)', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
+
+					fireKeyboardEvent( 'arrowright' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					fireDeleteEvent();
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><paragraph>foo</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a sibling widget', () => {
+					setModelData( editor.model, '<blockWidget><nested>foo</nested></blockWidget>[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowleft' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<blockWidget><nested>foo</nested></blockWidget>' +
+						'[<blockWidget></blockWidget>]'
+					);
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					fireDeleteEvent();
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph><blockWidget></blockWidget>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+			} );
+
+			describe( 'forward delete', () => {
+				it( 'should delete content after a widget if the "fake caret" is also after the widget', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
+
+					fireKeyboardEvent( 'arrowright' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					fireDeleteEvent( true );
+					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]oo</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should do nothing if the "fake caret" is after the widget but there is nothing to delete there', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowright' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					fireDeleteEvent( true );
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a widget if the "fake caret" is before the widget (no content before the widget)', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowleft' );
+
+					expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					fireDeleteEvent( true );
+					expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a widget if the "fake caret" is before the widget (some content before the widget)', () => {
+					setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+
+					fireKeyboardEvent( 'arrowleft' );
+
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+					fireDeleteEvent( true );
+					expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+
+				it( 'should delete a sibling widget', () => {
+					setModelData( editor.model, '[<blockWidget></blockWidget>]<blockWidget><nested>foo</nested></blockWidget>' );
+
+					fireKeyboardEvent( 'arrowright' );
+
+					expect( getModelData( model ) ).to.equal(
+						'[<blockWidget></blockWidget>]' +
+						'<blockWidget><nested>foo</nested></blockWidget>'
+					);
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+					fireDeleteEvent( true );
+					expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>[]</paragraph>' );
+					expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+					sinon.assert.calledOnce( eventInfoStub.stop );
+					sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+				} );
+			} );
+
+			function fireDeleteEvent( isForward = false ) {
+				eventInfoStub = new EventInfo( viewDocument, 'delete' );
+				sinon.spy( eventInfoStub, 'stop' );
+
+				const data = {
+					direction: isForward ? 'forward' : 'backward',
+					unit: 'character'
+				};
+
+				domEventDataStub = new DomEventData( viewDocument, getDomEvent(), data );
+
+				viewDocument.fire( eventInfoStub, domEventDataStub );
+			}
+		} );
+
 		function getDomEvent() {
 			return {
 				preventDefault: sinon.spy(),