8
0
Quellcode durchsuchen

Merge pull request #14 from ckeditor/t/13

Custom handling for delete.
Piotrek Koszuliński vor 9 Jahren
Ursprung
Commit
e7cc11cdff

+ 69 - 4
packages/ckeditor5-image/src/widget/widget.js

@@ -11,7 +11,10 @@ import Plugin from '../../core/plugin.js';
 import WidgetEngine from './widgetengine.js';
 import MouseObserver from '../../engine/view/observer/mouseobserver.js';
 import ModelRange from '../../engine/model/range.js';
+import ModelSelection from '../../engine/model/selection.js';
+import ModelElement from '../../engine/model/element.js';
 import { isWidget } from './utils.js';
+import { keyCodes } from '../../utils/keyboard.js';
 
 /**
  * The widget plugin.
@@ -36,6 +39,9 @@ export default class Widget extends Plugin {
 		// If mouse down is pressed on widget - create selection over whole widget.
 		viewDocument.addObserver( MouseObserver );
 		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
+
+		// Handle custom keydown behaviour.
+		this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
 	}
 
 	/**
@@ -67,12 +73,71 @@ export default class Widget extends Plugin {
 		}
 
 		// Create model selection over widget.
-		const modelDocument = editor.document;
 		const modelElement = editor.editing.mapper.toModelElement( widgetElement );
-		const modelRange = ModelRange.createOn( modelElement );
 
-		modelDocument.enqueueChanges( ( ) => {
-			modelDocument.selection.setRanges( [ modelRange ] );
+		editor.document.enqueueChanges( ( ) => {
+			this._setSelectionOverElement( modelElement );
 		} );
 	}
+
+	/**
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
+	 *
+	 * @private
+	 * @param {module:utils/eventinfo~EventInfo} eventInfo
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 */
+	_onKeydown( eventInfo, domEventData  ) {
+		const keyCode = domEventData.keyCode;
+
+		// Handle only delete and backspace.
+		if ( keyCode !== keyCodes.delete && keyCode !== keyCodes.backspace ) {
+			return;
+		}
+
+		const dataController = this.editor.data;
+		const modelDocument = this.editor.document;
+		const modelSelection = modelDocument.selection;
+
+		// Do nothing on non-collapsed selection.
+		if ( !modelSelection.isCollapsed ) {
+			return;
+		}
+
+		// Clone current selection to use it as a probe. We must leave default selection as it is so it can return
+		// to its current state after undo.
+		const probe = ModelSelection.createFromSelection( modelSelection );
+		const isForward = ( keyCode == keyCodes.delete );
+
+		dataController.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );
+
+		const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
+
+		if ( objectElement instanceof ModelElement && modelDocument.schema.objects.has( objectElement.name ) ) {
+			domEventData.preventDefault();
+			eventInfo.stop();
+
+			modelDocument.enqueueChanges( () => {
+				// Remove previous element if empty.
+				const previousNode = probe.anchor.parent;
+
+				if ( previousNode.isEmpty ) {
+					const batch = modelDocument.batch();
+					batch.remove( previousNode );
+				}
+
+				this._setSelectionOverElement( objectElement );
+			} );
+		}
+	}
+
+	/**
+	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element
+	 */
+	_setSelectionOverElement( element ) {
+		this.editor.document.selection.setRanges( [ ModelRange.createOn( element ) ] );
+	}
 }

+ 9 - 0
packages/ckeditor5-image/tests/manual/image.md

@@ -3,3 +3,12 @@
 * Two images with CKEditor logo should be loaded.
 * Hovering over image should apply yellow outline.
 * Clicking on image should apply blue outline which should not change when hovering over.
+
+### Deleting
+
+* Click image and press <kbd>delete</kbd> (<kbd>forward delete</kbd> on Mac) - empty paragraph should be created in place of image.
+* Click image and press <kbd>backspace</kbd> (<kbd>delete</kbd> on Mac)  - empty paragraph should be created in place of image.
+* Place selection at the end of the first paragraph and press <kbd>delete</kbd> (<kbd>forward delete</kbd> on Mac) - image should be selected. Second press should delete it.
+* Place selection at the beginning of the second paragraph and press <kbd>backspace</kbd> (<kbd>delete</kbd> on Mac) - image should be selected. Second press should delete it.
+* Place selection in an empty paragraph before image and press <kbd>delete</kbd> (<kbd>forward delete</kbd> on Mac) - image should be selected and paragraph removed.
+* Place selection in an empty paragraph after image and press <kbd>backspace</kbd> (<kbd>delete</kbd> on Mac) - image should be selected and paragraph removed.

+ 275 - 0
packages/ckeditor5-image/tests/widget/widget.js

@@ -9,8 +9,10 @@ import MouseObserver from 'ckeditor5/engine/view/observer/mouseobserver.js';
 import buildModelConverter from 'ckeditor5/engine/conversion/buildmodelconverter.js';
 import { widgetize } from 'ckeditor5/image/widget/utils.js';
 import ViewContainer from 'ckeditor5/engine/view/containerelement.js';
+import DomEventData from 'ckeditor5/engine/view/observer/domeventdata.js';
 import AttributeContainer from 'ckeditor5/engine/view/attributeelement.js';
 import { setData as setModelData, getData as getModelData } from 'ckeditor5/engine/dev-utils/model.js';
+import { keyCodes } from 'ckeditor5/utils/keyboard.js';
 
 describe( 'Widget', () => {
 	let editor, document, viewDocument;
@@ -25,7 +27,10 @@ describe( 'Widget', () => {
 				viewDocument = editor.editing.view;
 
 				document.schema.registerItem( 'widget', '$block' );
+				document.schema.objects.add( 'widget' );
 				document.schema.registerItem( 'paragraph', '$block' );
+				document.schema.registerItem( 'inline', '$inline' );
+				document.schema.objects.add( 'inline' );
 
 				buildModelConverter().for( editor.editing.modelToView )
 					.fromElement( 'paragraph' )
@@ -39,6 +44,10 @@ describe( 'Widget', () => {
 
 						return widgetize( div );
 					} );
+
+				buildModelConverter().for( editor.editing.modelToView )
+					.fromElement( 'inline' )
+					.toElement( 'figure' );
 			} );
 	} );
 
@@ -110,4 +119,270 @@ describe( 'Widget', () => {
 		sinon.assert.notCalled( focusSpy );
 		expect( getModelData( document ) ).to.equal( '[<widget></widget>]' );
 	} );
+
+	describe( 'delete and backspace handling', () => {
+		test(
+			'should select widget when backspace is pressed',
+			'<widget></widget><paragraph>[]foo</paragraph>',
+			keyCodes.backspace,
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should remove empty element after selecting widget when backspace is pressed',
+			'<widget></widget><paragraph>[]</paragraph>',
+			keyCodes.backspace,
+			'[<widget></widget>]'
+		);
+
+		test(
+			'should select widget when delete is pressed',
+			'<paragraph>foo[]</paragraph><widget></widget>',
+			keyCodes.delete,
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should remove empty element after selecting widget when delete is pressed',
+			'<paragraph>[]</paragraph><widget></widget>',
+			keyCodes.delete,
+			'[<widget></widget>]'
+		);
+
+		test(
+			'should not respond to other keys',
+			'<widget></widget><paragraph>[]foo</paragraph>',
+			65,
+			'<widget></widget><paragraph>[]foo</paragraph>'
+		);
+
+		test(
+			'should do nothing on non-collapsed selection',
+			'<widget></widget><paragraph>[f]oo</paragraph>',
+			keyCodes.backspace,
+			'<widget></widget><paragraph>[f]oo</paragraph>'
+		);
+
+		test(
+			'should do nothing on non-object elements',
+			'<paragraph>foo</paragraph><paragraph>[]bar</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>foo</paragraph><paragraph>[]bar</paragraph>'
+		);
+
+		test(
+			'should work correctly with modifier key: backspace + ctrl',
+			'<widget></widget><paragraph>[]foo</paragraph>',
+			{ keyCode: keyCodes.backspace, ctrlKey: true, preventDefault: () => {} },
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should work correctly with modifier key: backspace + alt',
+			'<widget></widget><paragraph>[]foo</paragraph>',
+			{ keyCode: keyCodes.backspace, altKey: true, preventDefault: () => {} },
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should work correctly with modifier key: backspace + shift',
+			'<widget></widget><paragraph>[]foo</paragraph>',
+			{ keyCode: keyCodes.backspace, shiftKey: true, preventDefault: () => {} },
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should work correctly with modifier key: delete + ctrl',
+			'<paragraph>foo[]</paragraph><widget></widget>',
+			{ keyCode: keyCodes.delete, ctrlKey: true, preventDefault: () => {} },
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should work correctly with modifier key: delete + alt',
+			'<paragraph>foo[]</paragraph><widget></widget>',
+			{ keyCode: keyCodes.delete, altKey: true, preventDefault: () => {} },
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should work correctly with modifier key: delete + shift',
+			'<paragraph>foo[]</paragraph><widget></widget>',
+			{ keyCode: keyCodes.delete, shiftKey: true, preventDefault: () => {} },
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should not modify backspace default behaviour in single paragraph boundaries',
+			'<paragraph>[]foo</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>[]foo</paragraph>'
+		);
+
+		test(
+			'should not modify delete default behaviour in single paragraph boundaries',
+			'<paragraph>foo[]</paragraph>',
+			keyCodes.delete,
+			'<paragraph>foo[]</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected widget preceded by a paragraph - backspace',
+			'<paragraph>foo</paragraph>[<widget></widget>]',
+			keyCodes.backspace,
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should do nothing on selected widget preceded by another widget - backspace',
+			'<widget></widget>[<widget></widget>]',
+			keyCodes.backspace,
+			'<widget></widget>[<widget></widget>]'
+		);
+
+		test(
+			'should do nothing on selected widget before paragraph - backspace',
+			'[<widget></widget>]<paragraph>foo</paragraph>',
+			keyCodes.backspace,
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected widget before another widget - backspace',
+			'[<widget></widget>]<widget></widget>',
+			keyCodes.backspace,
+			'[<widget></widget>]<widget></widget>'
+		);
+
+		test(
+			'should do nothing on selected widget between paragraphs - backspace',
+			'<paragraph>bar</paragraph>[<widget></widget>]<paragraph>foo</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>bar</paragraph>[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected widget between other widgets - backspace',
+			'<widget></widget>[<widget></widget>]<widget></widget>',
+			keyCodes.backspace,
+			'<widget></widget>[<widget></widget>]<widget></widget>'
+		);
+
+		test(
+			'should do nothing on selected widget preceded by a paragraph - delete',
+			'<paragraph>foo</paragraph>[<widget></widget>]',
+			keyCodes.delete,
+			'<paragraph>foo</paragraph>[<widget></widget>]'
+		);
+
+		test(
+			'should do nothing on selected widget preceded by another widget - delete',
+			'<widget></widget>[<widget></widget>]',
+			keyCodes.delete,
+			'<widget></widget>[<widget></widget>]'
+		);
+
+		test(
+			'should do nothing on selected widget before paragraph - delete',
+			'[<widget></widget>]<paragraph>foo</paragraph>',
+			keyCodes.delete,
+			'[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected widget before another widget - delete',
+			'[<widget></widget>]<widget></widget>',
+			keyCodes.delete,
+			'[<widget></widget>]<widget></widget>'
+		);
+
+		test(
+			'should do nothing on selected widget between paragraphs - delete',
+			'<paragraph>bar</paragraph>[<widget></widget>]<paragraph>foo</paragraph>',
+			keyCodes.delete,
+			'<paragraph>bar</paragraph>[<widget></widget>]<paragraph>foo</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected widget between other widgets - delete',
+			'<widget></widget>[<widget></widget>]<widget></widget>',
+			keyCodes.delete,
+			'<widget></widget>[<widget></widget>]<widget></widget>'
+		);
+
+		test(
+			'should select inline objects - backspace',
+			'<paragraph>foo<inline></inline>[]bar</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>foo[<inline></inline>]bar</paragraph>'
+		);
+
+		test(
+			'should select inline objects - delete',
+			'<paragraph>foo[]<inline></inline>bar</paragraph>',
+			keyCodes.delete,
+			'<paragraph>foo[<inline></inline>]bar</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected inline objects - backspace',
+			'<paragraph>foo[<inline></inline>]bar</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>foo[<inline></inline>]bar</paragraph>'
+		);
+
+		test(
+			'should do nothing on selected inline objects - delete',
+			'<paragraph>foo[<inline></inline>]bar</paragraph>',
+			keyCodes.delete,
+			'<paragraph>foo[<inline></inline>]bar</paragraph>'
+		);
+
+		test(
+			'should do nothing if selection is placed after first letter - backspace',
+			'<paragraph>a[]</paragraph>',
+			keyCodes.backspace,
+			'<paragraph>a[]</paragraph>'
+		);
+
+		test(
+			'should do nothing if selection is placed before first letter - delete',
+			'<paragraph>[]a</paragraph>',
+			keyCodes.delete,
+			'<paragraph>[]a</paragraph>'
+		);
+
+		it( 'should prevent default behaviour and stop event propagation', () => {
+			const keydownHandler = sinon.spy();
+			const domEventDataMock = {
+				keyCode: keyCodes.delete,
+				preventDefault: sinon.spy(),
+			};
+			setModelData( document, '<paragraph>foo[]</paragraph><widget></widget>' );
+			viewDocument.on( 'keydown',  keydownHandler );
+
+			viewDocument.fire( 'keydown', domEventDataMock );
+
+			expect( getModelData( document ) ).to.equal( '<paragraph>foo</paragraph>[<widget></widget>]' );
+			sinon.assert.calledOnce( domEventDataMock.preventDefault );
+			sinon.assert.notCalled( keydownHandler );
+		} );
+
+		function test( name, data, keyCodeOrMock, expected ) {
+			it( name, () => {
+				const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : {
+					keyCode: keyCodeOrMock
+				};
+
+				setModelData( document, data );
+				viewDocument.fire( 'keydown', new DomEventData(
+					viewDocument,
+					{ target: null, preventDefault: () => {} },
+					domEventDataMock
+				) );
+
+				expect( getModelData( document ) ).to.equal( expected );
+			} );
+		}
+	} );
 } );