Piotrek Koszuliński 9 years ago
parent
commit
de9fbbc5ad

+ 15 - 10
packages/ckeditor5-clipboard/src/clipboard.js

@@ -72,13 +72,12 @@ import HtmlDataProcessor from '../engine/dataprocessor/htmldataprocessor.js';
  * 1. {@link engine.controller.DataController#getSelectedContent get selected content} from the editor,
  * 2. prevent the default action of the native `copy` or `cut` event,
  * 3. fire {@link engine.view.Document#clipboardOutput} with a clone of the selected content
- * converted to a {@link engine.view.DocumentFragment view document fragment},
- * 4. in case of cut operation, delete the selected content.
+ * converted to a {@link engine.view.DocumentFragment view document fragment}.
  *
  * ### On {@link engine.view.Document#clipboardOutput}
  *
  * The default action is to put the content (`data.content`, represented by a {@link engine.view.DocumentFragment})
- * to the clipboard as HTML.
+ * to the clipboard as HTML. In case of the cut operation, the selected content is also deleted from the editor.
  *
  * This action is performed by a low priority listener, so it can be overridden by a normal one.
  *
@@ -150,13 +149,7 @@ export default class Clipboard extends Feature {
 
 			data.preventDefault();
 
-			editingView.fire( 'clipboardOutput', { dataTransfer, content } );
-
-			if ( evt.name == 'cut' ) {
-				doc.enqueueChanges( () => {
-					editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } );
-				} );
-			}
+			editingView.fire( 'clipboardOutput', { dataTransfer, content, method: evt.name } );
 		};
 
 		this.listenTo( editingView, 'copy', onCopyCut, { priority: 'low' } );
@@ -166,6 +159,12 @@ export default class Clipboard extends Feature {
 			if ( !data.content.isEmpty ) {
 				data.dataTransfer.setData( 'text/html', this._htmlDataProcessor.toData( data.content ) );
 			}
+
+			if ( data.method == 'cut' ) {
+				doc.enqueueChanges( () => {
+					editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } );
+				} );
+			}
 		}, { priority: 'low' } );
 	}
 }
@@ -230,3 +229,9 @@ export default class Clipboard extends Feature {
  *
  * @member {engine.view.DocumentFragment} engine.view.observer.ClipboardOutputEventData#content
  */
+
+/**
+ * Whether the event was triggered by copy or cut operation.
+ *
+ * @member {'copy'|'cut'} engine.view.observer.ClipboardOutputEventData#method
+ */

+ 147 - 9
packages/ckeditor5-clipboard/tests/clipboard.js

@@ -10,9 +10,14 @@ import Paragraph from 'ckeditor5/paragraph/paragraph.js';
 import ClipboardObserver from 'ckeditor5/clipboard/clipboardobserver.js';
 
 import { stringify as stringifyView } from 'ckeditor5/engine/dev-utils/view.js';
-import { stringify as stringifyModel } from 'ckeditor5/engine/dev-utils/model.js';
+import {
+	stringify as stringifyModel,
+	setData as setModelData,
+	getData as getModelData
+} from 'ckeditor5/engine/dev-utils/model.js';
 
 import ViewDocumentFragment from 'ckeditor5/engine/view/documentfragment.js';
+import ViewText from 'ckeditor5/engine/view/text.js';
 
 describe( 'Clipboard feature', () => {
 	let editor, editingView;
@@ -33,7 +38,7 @@ describe( 'Clipboard feature', () => {
 		} );
 	} );
 
-	describe( 'clipboard pipeline', () => {
+	describe( 'clipboard paste pipeline', () => {
 		it( 'takes HTML data from the dataTransfer', ( done ) => {
 			const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
 			const preventDefaultSpy = sinon.spy();
@@ -170,13 +175,146 @@ describe( 'Clipboard feature', () => {
 
 			expect( spy.callCount ).to.equal( 0 );
 		} );
+
+		function createDataTransfer( data ) {
+			return {
+				getData( type ) {
+					return data[ type ];
+				}
+			};
+		}
 	} );
-} );
 
-function createDataTransfer( data ) {
-	return {
-		getData( type ) {
-			return data[ type ];
+	describe( 'clipboard copy/cut pipeline', () => {
+		it( 'fires clipboardOutput for copy with the selected content and correct method', ( done ) => {
+			const dataTransferMock = createDataTransfer();
+			const preventDefaultSpy = sinon.spy();
+
+			setModelData( editor.document, '<paragraph>a[bc</paragraph><paragraph>de]f</paragraph>' );
+
+			editingView.on( 'clipboardOutput', ( evt, data ) => {
+				expect( preventDefaultSpy.calledOnce ).to.be.true;
+				expect( data.method ).to.equal( 'copy' );
+
+				expect( data.dataTransfer ).to.equal( dataTransferMock );
+
+				expect( data.content ).is.instanceOf( ViewDocumentFragment );
+				expect( stringifyView( data.content ) ).to.equal( '<p>bc</p><p>de</p>' );
+
+				done();
+			} );
+
+			editingView.fire( 'copy', {
+				dataTransfer: dataTransferMock,
+				preventDefault: preventDefaultSpy
+			} );
+		} );
+
+		it( 'fires clipboardOutput for cut with the selected content and correct method', ( done ) => {
+			const dataTransferMock = createDataTransfer();
+			const preventDefaultSpy = sinon.spy();
+
+			setModelData( editor.document, '<paragraph>a[bc</paragraph><paragraph>de]f</paragraph>' );
+
+			editingView.on( 'clipboardOutput', ( evt, data ) => {
+				expect( data.method ).to.equal( 'cut' );
+
+				done();
+			} );
+
+			editingView.fire( 'cut', {
+				dataTransfer: dataTransferMock,
+				preventDefault: preventDefaultSpy
+			} );
+		} );
+
+		it( 'uses low priority observer for the copy event', () => {
+			const dataTransferMock = createDataTransfer();
+			const spy = sinon.spy();
+
+			editingView.on( 'copy', ( evt ) => {
+				evt.stop();
+			} );
+
+			editingView.on( 'clipboardOutput', spy );
+
+			editingView.fire( 'copy', {
+				dataTransfer: dataTransferMock,
+				preventDefault() {}
+			} );
+
+			expect( spy.callCount ).to.equal( 0 );
+		} );
+
+		it( 'sets clipboard HTML data', () => {
+			const dataTransferMock = createDataTransfer();
+
+			setModelData( editor.document, '<paragraph>f[o]o</paragraph>' );
+
+			editingView.fire( 'clipboardOutput', {
+				dataTransfer: dataTransferMock,
+				content: new ViewDocumentFragment( [ new ViewText( 'abc' ) ] ),
+				method: 'copy'
+			} );
+
+			expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'abc' );
+			expect( getModelData( editor.document ) ).to.equal( '<paragraph>f[o]o</paragraph>' );
+		} );
+
+		it( 'does not set clipboard HTML data if content is empty', () => {
+			const dataTransferMock = createDataTransfer();
+
+			editingView.fire( 'clipboardOutput', {
+				dataTransfer: dataTransferMock,
+				content: new ViewDocumentFragment(),
+				method: 'copy'
+			} );
+
+			expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined;
+		} );
+
+		it( 'deletes selected content in case of cut', () => {
+			const dataTransferMock = createDataTransfer();
+
+			setModelData( editor.document, '<paragraph>f[o</paragraph><paragraph>x]o</paragraph>' );
+
+			editingView.fire( 'clipboardOutput', {
+				dataTransfer: dataTransferMock,
+				content: new ViewDocumentFragment(),
+				method: 'cut'
+			} );
+
+			expect( getModelData( editor.document ) ).to.equal( '<paragraph>f[]o</paragraph>' );
+		} );
+
+		it( 'uses low priority observer for the clipboardOutput event', () => {
+			const dataTransferMock = createDataTransfer();
+
+			editingView.on( 'clipboardOutput', ( evt ) => {
+				evt.stop();
+			} );
+
+			editingView.fire( 'copy', {
+				dataTransfer: dataTransferMock,
+				content: new ViewDocumentFragment( [ new ViewText( 'abc' ) ] ),
+				preventDefault() {}
+			} );
+
+			expect( dataTransferMock.getData( 'text/html' ) ).to.be.undefined;
+		} );
+
+		function createDataTransfer() {
+			const store = new Map();
+
+			return {
+				setData( type, data ) {
+					store.set( type, data );
+				},
+
+				getData( type ) {
+					return store.get( type );
+				}
+			};
 		}
-	};
-}
+	} );
+} );

+ 3 - 1
packages/ckeditor5-clipboard/tests/clipboardobserver.js

@@ -18,7 +18,7 @@ describe( 'ClipboardObserver', () => {
 	} );
 
 	it( 'should define domEventType', () => {
-		expect( observer.domEventType ).to.equal( 'paste' );
+		expect( observer.domEventType ).to.deep.equal( [ 'paste', 'copy', 'cut' ] );
 	} );
 
 	describe( 'onDomEvent', () => {
@@ -41,5 +41,7 @@ describe( 'ClipboardObserver', () => {
 			expect( data.dataTransfer ).to.be.instanceOf( DataTransfer );
 			expect( data.dataTransfer.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
 		} );
+
+		// If it fires paste it fires all the other events too.
 	} );
 } );

+ 21 - 6
packages/ckeditor5-clipboard/tests/datatransfer.js

@@ -6,13 +6,28 @@
 import DataTransfer from 'ckeditor5/clipboard/datatransfer.js';
 
 describe( 'DataTransfer', () => {
-	it( 'should return data from the native data transfer', () => {
-		const dt = new DataTransfer( {
-			getData( type ) {
-				return 'foo:' + type;
-			}
+	describe( 'getData', () => {
+		it( 'should return data from the native data transfer', () => {
+			const dt = new DataTransfer( {
+				getData( type ) {
+					return 'foo:' + type;
+				}
+			} );
+
+			expect( dt.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
 		} );
+	} );
+
+	describe( 'setData', () => {
+		it( 'should return set data in the native data transfer', () => {
+			const spy = sinon.spy();
+			const dt = new DataTransfer( {
+				setData: spy
+			} );
 
-		expect( dt.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
+			dt.setData( 'text/html', 'bar' );
+
+			expect( spy.calledWithExactly( 'text/html', 'bar' ) ).to.be.true;
+		} );
 	} );
 } );