Преглед изворни кода

Merge pull request #7424 from ckeditor/i/6696

Internal (widget): Integrated the "fake widget caret" state with the `Model#insertContent` method. Closes #6696.
Aleksander Nowodzinski пре 5 година
родитељ
комит
599040faa6

+ 64 - 19
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -87,6 +87,7 @@ export default class WidgetTypeAround extends Plugin {
 		this._enableInsertingParagraphsOnTypingKeystroke();
 		this._enableInsertingParagraphsOnTypingKeystroke();
 		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
 		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
 		this._enableDeleteIntegration();
 		this._enableDeleteIntegration();
+		this._enableInsertContentIntegration();
 	}
 	}
 
 
 	/**
 	/**
@@ -109,16 +110,9 @@ export default class WidgetTypeAround extends Plugin {
 	_insertParagraph( widgetModelElement, position ) {
 	_insertParagraph( widgetModelElement, position ) {
 		const editor = this.editor;
 		const editor = this.editor;
 		const editingView = editor.editing.view;
 		const editingView = editor.editing.view;
-		let modelPosition;
-
-		if ( position === 'before' ) {
-			modelPosition = editor.model.createPositionBefore( widgetModelElement );
-		} else {
-			modelPosition = editor.model.createPositionAfter( widgetModelElement );
-		}
 
 
 		editor.execute( 'insertParagraph', {
 		editor.execute( 'insertParagraph', {
-			position: modelPosition
+			position: editor.model.createPositionAt( widgetModelElement, position )
 		} );
 		} );
 
 
 		editingView.focus();
 		editingView.focus();
@@ -235,14 +229,26 @@ export default class WidgetTypeAround extends Plugin {
 				return;
 				return;
 			}
 			}
 
 
-			const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+			// Get rid of the widget type around attribute of the selection on every change:range.
+			// If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
+			editor.model.change( writer => {
+				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+			} );
+		} );
 
 
-			if ( !typeAroundSelectionAttribute ) {
-				return;
+		// Get rid of the widget type around attribute of the selection on every document change
+		// that makes widget not selected any more (i.e. widget was removed).
+		model.document.on( 'change:data', () => {
+			const selectedModelElement = modelSelection.getSelectedElement();
+
+			if ( selectedModelElement ) {
+				const selectedViewElement = editor.editing.mapper.toViewElement( selectedModelElement );
+
+				if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
+					return;
+				}
 			}
 			}
 
 
-			// Get rid of the widget type around attribute of the selection on every change:range.
-			// If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
 			editor.model.change( writer => {
 			editor.model.change( writer => {
 				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 			} );
 			} );
@@ -362,9 +368,8 @@ export default class WidgetTypeAround extends Plugin {
 		const model = editor.model;
 		const model = editor.model;
 		const modelSelection = model.document.selection;
 		const modelSelection = model.document.selection;
 		const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 		const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
-		let shouldStopAndPreventDefault = false;
 
 
-		model.change( writer => {
+		return model.change( writer => {
 			// If the selection already has the attribute...
 			// If the selection already has the attribute...
 			if ( typeAroundSelectionAttribute ) {
 			if ( typeAroundSelectionAttribute ) {
 				const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' );
 				const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' );
@@ -380,7 +385,7 @@ export default class WidgetTypeAround extends Plugin {
 				if ( !isLeavingWidget ) {
 				if ( !isLeavingWidget ) {
 					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
 
 
-					shouldStopAndPreventDefault = true;
+					return true;
 				}
 				}
 			}
 			}
 			// If the selection didn't have the attribute, let's set it now according to the direction of the arrow
 			// If the selection didn't have the attribute, let's set it now according to the direction of the arrow
@@ -388,11 +393,11 @@ export default class WidgetTypeAround extends Plugin {
 			else {
 			else {
 				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );
 				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );
 
 
-				shouldStopAndPreventDefault = true;
+				return true;
 			}
 			}
-		} );
 
 
-		return shouldStopAndPreventDefault;
+			return false;
+		} );
 	}
 	}
 
 
 	/**
 	/**
@@ -628,6 +633,46 @@ export default class WidgetTypeAround extends Plugin {
 			evt.stop();
 			evt.stop();
 		}, { priority: priorities.get( 'high' ) + 1 } );
 		}, { priority: priorities.get( 'high' ) + 1 } );
 	}
 	}
+
+	/**
+	 * Attaches the {@link module:engine/model/model~Model#event:insertContent} event listener that, for instance, allows the user to paste
+	 * content near a widget when the "fake caret" was first activated using the arrow keys.
+	 *
+	 * The content is inserted according to the "widget-type-around" selection attribute (see {@link #_handleArrowKeyPress}).
+	 *
+	 * @private
+	 */
+	_enableInsertContentIntegration() {
+		const editor = this.editor;
+		const model = this.editor.model;
+		const documentSelection = model.document.selection;
+
+		this.listenTo( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
+			if ( selectable && !selectable.is( 'documentSelection' ) ) {
+				return;
+			}
+
+			const typeAroundSelectionAttributeValue = documentSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
+
+			if ( !typeAroundSelectionAttributeValue ) {
+				return;
+			}
+
+			evt.stop();
+
+			return model.change( writer => {
+				const selectedElement = documentSelection.getSelectedElement();
+				const position = model.createPositionAt( selectedElement, typeAroundSelectionAttributeValue );
+				const selection = writer.createSelection( position );
+
+				const result = model.insertContent( content, selection );
+
+				writer.setSelection( selection );
+
+				return result;
+			} );
+		}, { priority: 'high' } );
+	}
 }
 }
 
 
 // Injects the type around UI into a view widget instance.
 // Injects the type around UI into a view widget instance.

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

@@ -734,6 +734,36 @@ describe( 'WidgetTypeAround', () => {
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
 		} );
 		} );
 
 
+		it( 'should quit the "fake caret" mode when model was changed (model.deleteContent())', () => {
+			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
+
+			const selection = model.createSelection( modelSelection );
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				model.deleteContent( selection );
+			} );
+
+			const viewWidget = viewRoot.getChild( 1 );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph></paragraph><paragraph>baz</paragraph>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+			expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+		} );
+
+		it( 'should quit the "fake caret" mode when model was changed (writer.remove())', () => {
+			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				writer.remove( editor.model.document.getRoot().getChild( 1 ) );
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>baz</paragraph>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+		} );
+
 		describe( 'inserting a new paragraph', () => {
 		describe( 'inserting a new paragraph', () => {
 			describe( 'on Enter key press when the "fake caret" is activated', () => {
 			describe( 'on Enter key press when the "fake caret" is activated', () => {
 				it( 'should insert a paragraph before a widget if the caret was "before" it', () => {
 				it( 'should insert a paragraph before a widget if the caret was "before" it', () => {
@@ -1317,6 +1347,130 @@ describe( 'WidgetTypeAround', () => {
 		}
 		}
 	} );
 	} );
 
 
+	describe( 'Model#insertContent() integration', () => {
+		let model, modelSelection;
+
+		beforeEach( () => {
+			model = editor.model;
+			modelSelection = model.document.selection;
+		} );
+
+		it( 'should not alter insertContent for the selection other than the document selection', () => {
+			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
+
+			const batchSet = setupBatchWatch();
+			const selection = model.createSelection( modelSelection );
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+				model.insertContent( createParagraph( 'bar' ), selection );
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>bar</paragraph><paragraph>baz</paragraph>' );
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		it( 'should not alter insertContent when the "fake caret" is not active', () => {
+			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );
+
+			const batchSet = setupBatchWatch();
+
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><paragraph>baz</paragraph>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		it( 'should handle insertContent before a widget when it\'s the first element of the root', () => {
+			setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+			const batchSet = setupBatchWatch();
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+			} );
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		it( 'should handle insertContent after a widget when it\'s the last element of the root', () => {
+			setModelData( editor.model, '[<blockWidget></blockWidget>]' );
+
+			const batchSet = setupBatchWatch();
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'after' );
+			} );
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		it( 'should handle insertContent before a widget when it\'s not the first element of the root', () => {
+			setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+
+			const batchSet = setupBatchWatch();
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'before' );
+			} );
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		it( 'should handle insertContent after a widget when it\'s not the last element of the root', () => {
+			setModelData( editor.model, '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
+
+			const batchSet = setupBatchWatch();
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'widget-type-around', 'after' );
+			} );
+
+			model.insertContent( createParagraph( 'bar' ) );
+
+			expect( getModelData( model ) ).to.equal( '<blockWidget></blockWidget><paragraph>bar[]</paragraph><paragraph>foo</paragraph>' );
+			expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+			expect( batchSet.size ).to.be.equal( 1 );
+		} );
+
+		function createParagraph( text ) {
+			return model.change( writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+
+				writer.insertText( text, paragraph );
+
+				return paragraph;
+			} );
+		}
+
+		function setupBatchWatch() {
+			const createdBatches = new Set();
+
+			model.on( 'applyOperation', ( evt, [ operation ] ) => {
+				if ( operation.isDocumentOperation ) {
+					createdBatches.add( operation.batch );
+				}
+			} );
+
+			return createdBatches;
+		}
+	} );
+
 	function blockWidgetPlugin( editor ) {
 	function blockWidgetPlugin( editor ) {
 		editor.model.schema.register( 'blockWidget', {
 		editor.model.schema.register( 'blockWidget', {
 			inheritAllFrom: '$block',
 			inheritAllFrom: '$block',