8
0
Просмотр исходного кода

Merge pull request #17 from ckeditor/t/16

Feature: Introduced DataTransfer#files property. Change the clipboard input pipeline. Closes #16.

BREAKING CHANGE: The `clipboardInput` event now contains only `DataTransfer`, no `content` there anymore. The separate `inputTransformation` event was introduced for the content transformations.
Piotr Jasiun 8 лет назад
Родитель
Сommit
f9270bf22d

+ 29 - 35
packages/ckeditor5-clipboard/src/clipboard.js

@@ -17,7 +17,7 @@ import normalizeClipboardHtml from './utils/normalizeclipboarddata';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 
 /**
- * The clipboard feature. Currently, it's only responsible for intercepting the `paste` event and
+ * The clipboard feature. Currently, it's responsible for intercepting the `paste` and `drop` events and
  * passing the pasted content through the clipboard pipeline.
  *
  * ## Clipboard input pipeline
@@ -26,22 +26,36 @@ import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/html
  * before it gets inserted into the editor. The pipeline consists of two events on which
  * the features can listen in order to modify or totally override the default behavior.
  *
- * ### On {@link module:engine/view/document~Document#event:paste}
+ * ### On {@link module:engine/view/document~Document#event:paste} and {@link module:engine/view/document~Document#event:drop}
  *
  * The default action is to:
  *
  * 1. get HTML or plain text from the clipboard,
- * 2. prevent the default action of the native `paste` event,
- * 3. fire {@link module:engine/view/document~Document#event:clipboardInput} with the clipboard data parsed to
+ * 2. prevent the default action of the native `paste` or `drop` event,
+ * 3. fire {@link module:engine/view/document~Document#event:clipboardInput} with a
+ * {@link module:clipboard/datatransfer~DataTransfer `dataTransfer`} property.
+ * 4. fire {@link module:clipboard/clipboard~Clipboard#event:inputTransformation} with a `data` containing the clipboard data parsed to
  * a {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
  *
- * This action is performed by a low priority listener, so it can be overridden by a normal one
+ * These action are performed by a low priority listeners, so they can be overridden by a normal ones
  * when a deeper change in pasting behavior is needed. For example, a feature which wants to differently read
  * data from the clipboard (the {@link module:clipboard/datatransfer~DataTransfer `DataTransfer`}).
  * should plug a listener at this stage.
  *
  * ### On {@link module:engine/view/document~Document#event:clipboardInput}
  *
+ * This action is performed by a low priority listener, so it can be overridden by a normal one.
+ *
+ * At this stage the dataTransfer object can be processed by the features, which want to transform the original dataTransform.
+ *
+ *		this.listenTo( editor.editing.view, 'clipboardInput', ( evt, data ) => {
+ *			const content = customTransform( data.dataTransfer.get( 'text/html' ) );
+ *			const transformedContent = transform( content );
+ *			data.dataTransfer.set( 'text/html', transformedContent );
+ *		} );
+ *
+ * ### On {@link module:clipboard/clipboard~Clipboard#event:inputTransformation}
+ *
  * The default action is to insert the content (`data.content`, represented by a
  * {@link module:engine/view/documentfragment~DocumentFragment}) to an editor if the data is not empty.
  *
@@ -50,7 +64,7 @@ import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/html
  * At this stage the pasted content can be processed by the features. E.g. a feature which wants to transform
  * a pasted text into a link can be implemented in this way:
  *
- *		this.listenTo( editor.editing.view, 'clipboardInput', ( evt, data ) => {
+ *		this.listenTo( editor.plugins.get( 'clipboard/clipboard' ), 'inputTransformation', ( evt, data ) => {
  *			if ( data.content.childCount == 1 && isUrlText( data.content.getChild( 0 ) ) ) {
  *				const linkUrl = data.content.getChild( 0 ).data;
  *
@@ -118,7 +132,7 @@ export default class Clipboard extends Plugin {
 
 		// The clipboard paste pipeline.
 
-		this.listenTo( editingView, 'paste', ( evt, data ) => {
+		this.listenTo( editingView, 'clipboardInput', ( evt, data ) => {
 			const dataTransfer = data.dataTransfer;
 			let content = '';
 
@@ -130,17 +144,15 @@ export default class Clipboard extends Plugin {
 
 			content = this._htmlDataProcessor.toView( content );
 
-			data.preventDefault();
-
-			editingView.fire( 'clipboardInput', { dataTransfer, content } );
+			this.fire( 'inputTransformation', { content } );
 		}, { priority: 'low' } );
 
-		this.listenTo( editingView, 'clipboardInput', ( evt, data ) => {
+		this.listenTo( this, 'inputTransformation', ( evt, data ) => {
 			if ( !data.content.isEmpty ) {
 				const dataController = this.editor.data;
 
 				// Convert the pasted content to a model document fragment.
-				// Convertion is contextual, but in this case we need an "all allowed" context and for that
+				// Conversion is contextual, but in this case we need an "all allowed" context and for that
 				// we use the $clipboardHolder item.
 				const modelFragment = dataController.toModel( data.content, '$clipboardHolder' );
 
@@ -179,34 +191,16 @@ export default class Clipboard extends Plugin {
 }
 
 /**
- * Fired with a content which comes from the clipboard (was pasted or dropped) and
+ * Fired with a `content`, which comes from the clipboard (was pasted or dropped) and
  * should be processed in order to be inserted into the editor.
  * It's part of the {@link module:clipboard/clipboard~Clipboard "clipboard pipeline"}.
  *
  * @see module:clipboard/clipboardobserver~ClipboardObserver
  * @see module:clipboard/clipboard~Clipboard
- * @event module:engine/view/document~Document#event:clipboardInput
- * @param {module:clipboard/clipboard~ClipboardInputEventData} data Event data.
- */
-
-/**
- * The value of the {@link module:engine/view/document~Document#event:clipboardInput} event.
- *
- * @class module:clipboard/clipboard~ClipboardInputEventData
- */
-
-/**
- * Data transfer instance.
- *
- * @readonly
- * @member {module:clipboard/datatransfer~DataTransfer} module:clipboard/clipboard~ClipboardInputEventData#dataTransfer
- */
-
-/**
- * Content to be inserted into the editor. It can be modified by the event listeners.
- * Read more about the clipboard pipelines in {@link module:clipboard/clipboard~Clipboard}.
- *
- * @member {module:engine/view/documentfragment~DocumentFragment} module:clipboard/clipboard~ClipboardInputEventData#content
+ * @event module:clipboard/clipboard~Clipboard#event:inputTransformation
+ * @param {Object} data Event data.
+ * @param {module:engine/view/documentfragment~DocumentFragment} data.content Event data. Content to be inserted into the editor.
+ * It can be modified by the event listeners. Read more about the clipboard pipelines in {@link module:clipboard/clipboard~Clipboard}
  */
 
 /**

+ 37 - 2
packages/ckeditor5-clipboard/src/clipboardobserver.js

@@ -22,16 +22,51 @@ export default class ClipboardObserver extends DomEventObserver {
 	constructor( doc ) {
 		super( doc );
 
-		this.domEventType = [ 'paste', 'copy', 'cut' ];
+		this.domEventType = [ 'paste', 'copy', 'cut', 'drop' ];
+
+		this.listenTo( doc, 'paste', handleInput, { priority: 'low' } );
+		this.listenTo( doc, 'drop', handleInput, { priority: 'low' } );
+
+		function handleInput( evt, data ) {
+			data.preventDefault();
+
+			doc.fire( 'clipboardInput', { dataTransfer: data.dataTransfer } );
+		}
 	}
 
 	onDomEvent( domEvent ) {
 		this.fire( domEvent.type, domEvent, {
-			dataTransfer: new DataTransfer( domEvent.clipboardData )
+			dataTransfer: new DataTransfer( domEvent.clipboardData ? domEvent.clipboardData : domEvent.dataTransfer )
 		} );
 	}
 }
 
+/**
+ * Fired with a `dataTransfer`, which comes from the clipboard (was pasted or dropped) and
+ * should be processed in order to be inserted into the editor.
+ * It's part of the {@link module:clipboard/clipboard~Clipboard "clipboard pipeline"}.
+ *
+ * @see module:clipboard/clipboardobserver~ClipboardObserver
+ * @see module:clipboard/clipboard~Clipboard
+ * @event module:engine/view/document~Document#event:input
+ * @param {Object} data Event data.
+ * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer Data transfer instance.
+ */
+
+/**
+ * Fired when user dropped content into one of the editables.
+ *
+ * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
+ *
+ * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/document~Document#addObserver} method.
+ * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ *
+ * @see module:clipboard/clipboardobserver~ClipboardObserver
+ * @event module:engine/view/document~Document#event:drop
+ * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ */
+
 /**
  * Fired when user pasted content into one of the editables.
  *

+ 31 - 0
packages/ckeditor5-clipboard/src/datatransfer.js

@@ -12,6 +12,14 @@
  */
 export default class DataTransfer {
 	constructor( nativeDataTransfer ) {
+		/**
+		 * The array of files created from the native `DataTransfer#files` or `DataTransfer#items`.
+		 *
+		 * @readonly
+		 * @member {Array.<File>} #files
+		 */
+		this.files = getFiles( nativeDataTransfer );
+
 		/**
 		 * The native DataTransfer object.
 		 *
@@ -21,6 +29,15 @@ export default class DataTransfer {
 		this._native = nativeDataTransfer;
 	}
 
+	/**
+	 * Returns an array of available native content types.
+	 *
+	 * @returns {Array.<String>}
+	 */
+	get types() {
+		return this._native.types;
+	}
+
 	/**
 	 * Gets data from the data transfer by its mime type.
 	 *
@@ -43,3 +60,17 @@ export default class DataTransfer {
 		this._native.setData( type, data );
 	}
 }
+
+function getFiles( nativeDataTransfer ) {
+	// DataTransfer.files and items are Array-like and might not have an iterable interface.
+	const files = nativeDataTransfer.files ? Array.from( nativeDataTransfer.files ) : [];
+	const items = nativeDataTransfer.items ? Array.from( nativeDataTransfer.items ) : [];
+
+	if ( files.length ) {
+		return files;
+	}
+	// Chrome have empty DataTransfer.files, but let get files through the items interface.
+	return items
+		.filter( item => item.kind === 'file' )
+		.map( item => item.getAsFile() );
+}

+ 67 - 29
packages/ckeditor5-clipboard/tests/clipboard.js

@@ -20,7 +20,7 @@ import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfr
 import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 
 describe( 'Clipboard feature', () => {
-	let editor, editingView;
+	let editor, editingView, clipboardPlugin;
 
 	beforeEach( () => {
 		return VirtualTestEditor.create( {
@@ -29,6 +29,7 @@ describe( 'Clipboard feature', () => {
 			.then( ( newEditor ) => {
 				editor = newEditor;
 				editingView = editor.editing.view;
+				clipboardPlugin = editor.plugins.get( 'clipboard/clipboard' );
 			} );
 	} );
 
@@ -39,60 +40,97 @@ describe( 'Clipboard feature', () => {
 	} );
 
 	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();
+		describe( 'takes HTML data from the dataTransfer', () => {
+			it( 'and fires the clipboardInput event on the editingView', ( done ) => {
+				const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
+				const preventDefaultSpy = sinon.spy();
+
+				editingView.on( 'clipboardInput', ( evt, data ) => {
+					expect( preventDefaultSpy.calledOnce ).to.be.true;
+					expect( data.dataTransfer ).to.equal( dataTransferMock );
+
+					done();
+				} );
+
+				editingView.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
+			} );
 
-			editingView.on( 'clipboardInput', ( evt, data ) => {
-				expect( preventDefaultSpy.calledOnce ).to.be.true;
+			it( 'and fires the inputTransformation event on the clipboardPlugin', ( done ) => {
+				const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
+				const preventDefaultSpy = sinon.spy();
 
-				expect( data.dataTransfer ).to.equal( dataTransferMock );
+				clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
+					expect( data.content ).is.instanceOf( ViewDocumentFragment );
+					expect( stringifyView( data.content ) ).to.equal( '<p>x</p>' );
 
-				expect( data.content ).is.instanceOf( ViewDocumentFragment );
-				expect( stringifyView( data.content ) ).to.equal( '<p>x</p>' );
+					done();
+				} );
 
-				done();
-			} );
-
-			editingView.fire( 'paste', {
-				dataTransfer: dataTransferMock,
-				preventDefault: preventDefaultSpy
+				editingView.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
 			} );
 		} );
 
-		it( 'takes plain text data from the dataTransfer if there is no HTML', ( done ) => {
-			const dataTransferMock = createDataTransfer( { 'text/plain': 'x\n\ny  z' } );
-			const preventDefaultSpy = sinon.spy();
+		describe( 'takes plain text data from the dataTransfer if there is no HTML', () => {
+			it( 'and fires the clipboardInput event on the editingView', ( done ) => {
+				const dataTransferMock = createDataTransfer( { 'text/plain': 'x\n\ny  z' } );
+				const preventDefaultSpy = sinon.spy();
 
-			editingView.on( 'clipboardInput', ( evt, data ) => {
-				expect( preventDefaultSpy.calledOnce ).to.be.true;
-
-				expect( data.dataTransfer ).to.equal( dataTransferMock );
+				editingView.on( 'clipboardInput', ( evt, data ) => {
+					expect( preventDefaultSpy.calledOnce ).to.be.true;
+					expect( data.dataTransfer ).to.equal( dataTransferMock );
 
-				expect( data.content ).is.instanceOf( ViewDocumentFragment );
-				expect( stringifyView( data.content ) ).to.equal( '<p>x</p><p>y  z</p>' );
+					done();
+				} );
 
-				done();
+				editingView.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
 			} );
 
-			editingView.fire( 'paste', {
-				dataTransfer: dataTransferMock,
-				preventDefault: preventDefaultSpy
+			it( 'and fires the inputTransformation event on the clipboardPlugin', ( done ) => {
+				const dataTransferMock = createDataTransfer( { 'text/plain': 'x\n\ny  z' } );
+				const preventDefaultSpy = sinon.spy();
+
+				clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
+					expect( data.content ).is.instanceOf( ViewDocumentFragment );
+					expect( stringifyView( data.content ) ).to.equal( '<p>x</p><p>y  z</p>' );
+
+					done();
+				} );
+
+				editingView.fire( 'paste', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
 			} );
 		} );
 
-		it( 'fires clipboardInput event with empty data if there is no HTML nor plain text', ( done ) => {
+		it( 'fires events with empty data if there is no HTML nor plain text', ( done ) => {
 			const dataTransferMock = createDataTransfer( {} );
 			const preventDefaultSpy = sinon.spy();
+			const editorViewCalled = sinon.spy();
 
 			editingView.on( 'clipboardInput', ( evt, data ) => {
 				expect( preventDefaultSpy.calledOnce ).to.be.true;
 
 				expect( data.dataTransfer ).to.equal( dataTransferMock );
 
+				editorViewCalled();
+			} );
+
+			clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
 				expect( data.content ).is.instanceOf( ViewDocumentFragment );
 				expect( stringifyView( data.content ) ).to.equal( '' );
 
+				expect( editorViewCalled.calledOnce ).to.be.true;
+
 				done();
 			} );
 

+ 44 - 9
packages/ckeditor5-clipboard/tests/clipboardobserver.js

@@ -18,30 +18,65 @@ describe( 'ClipboardObserver', () => {
 	} );
 
 	it( 'should define domEventType', () => {
-		expect( observer.domEventType ).to.deep.equal( [ 'paste', 'copy', 'cut' ] );
+		expect( observer.domEventType ).to.deep.equal( [ 'paste', 'copy', 'cut', 'drop' ] );
 	} );
 
 	describe( 'onDomEvent', () => {
-		it( 'should fire paste with the right event data', () => {
-			const spy = sinon.spy();
-			const dataTransfer = {
+		let pasteSpy, preventDefaultSpy;
+
+		function getDataTransfer() {
+			return {
 				getData( type ) {
 					return 'foo:' + type;
 				}
 			};
+		}
+
+		beforeEach( () => {
+			pasteSpy = sinon.spy();
+			preventDefaultSpy = sinon.spy();
+		} );
 
-			viewDocument.on( 'paste', spy );
+		it( 'should fire paste with the right event data - clipboardData', () => {
+			const dataTransfer = getDataTransfer();
 
-			observer.onDomEvent( { type: 'paste', target: document.body, clipboardData: dataTransfer } );
+			viewDocument.on( 'paste', pasteSpy );
 
-			expect( spy.calledOnce ).to.be.true;
+			observer.onDomEvent( {
+				type: 'paste',
+				target: document.body,
+				clipboardData: dataTransfer,
+				preventDefault: preventDefaultSpy
+			} );
 
-			const data = spy.args[ 0 ][ 1 ];
+			expect( pasteSpy.calledOnce ).to.be.true;
+
+			const data = pasteSpy.args[ 0 ][ 1 ];
 			expect( data.domTarget ).to.equal( document.body );
 			expect( data.dataTransfer ).to.be.instanceOf( DataTransfer );
 			expect( data.dataTransfer.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
+			expect( preventDefaultSpy.calledOnce ).to.be.true;
 		} );
 
-		// If it fires paste it fires all the other events too.
+		it( 'should fire paste with the right event data - dataTransfer', () => {
+			const dataTransfer = getDataTransfer();
+
+			viewDocument.on( 'drop', pasteSpy );
+
+			observer.onDomEvent( {
+				type: 'drop',
+				target: document.body,
+				dataTransfer,
+				preventDefault: preventDefaultSpy
+			} );
+
+			expect( pasteSpy.calledOnce ).to.be.true;
+
+			const data = pasteSpy.args[ 0 ][ 1 ];
+			expect( data.domTarget ).to.equal( document.body );
+			expect( data.dataTransfer ).to.be.instanceOf( DataTransfer );
+			expect( data.dataTransfer.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
+			expect( preventDefaultSpy.calledOnce ).to.be.true;
+		} );
 	} );
 } );

+ 39 - 2
packages/ckeditor5-clipboard/tests/datatransfer.js

@@ -6,7 +6,34 @@
 import DataTransfer from '../src/datatransfer';
 
 describe( 'DataTransfer', () => {
-	describe( 'getData', () => {
+	describe( 'constructor', () => {
+		it( 'should create files from the native files', () => {
+			const dt = new DataTransfer( {
+				files: {
+					0: 'file1',
+					1: 'file2',
+					length: 2
+				}
+			} );
+
+			expect( dt.files ).to.deep.equal( [ 'file1', 'file2' ] );
+		} );
+
+		it( 'should create files from the native items', () => {
+			const dt = new DataTransfer( {
+				items: {
+					0: { kind: 'file', getAsFile: () => 'file1' },
+					1: { kind: 'file', getAsFile: () => 'file2' },
+					2: { kind: 'someOtherKind' },
+					length: 3
+				},
+				files: []
+			} );
+
+			expect( dt.files ).to.deep.equal( [ 'file1', 'file2' ] );
+		} );
+	} );
+	describe( 'getData()', () => {
 		it( 'should return data from the native data transfer', () => {
 			const dt = new DataTransfer( {
 				getData( type ) {
@@ -18,7 +45,7 @@ describe( 'DataTransfer', () => {
 		} );
 	} );
 
-	describe( 'setData', () => {
+	describe( 'setData()', () => {
 		it( 'should return set data in the native data transfer', () => {
 			const spy = sinon.spy();
 			const dt = new DataTransfer( {
@@ -30,4 +57,14 @@ describe( 'DataTransfer', () => {
 			expect( spy.calledWithExactly( 'text/html', 'bar' ) ).to.be.true;
 		} );
 	} );
+
+	describe( 'types', () => {
+		it( 'should return available types', () => {
+			const dt = new DataTransfer( {
+				types: [ 'text/html', 'text/plain' ]
+			} );
+
+			expect( dt.types ).to.deep.equal( [ 'text/html', 'text/plain' ] );
+		} );
+	} );
 } );

+ 2 - 1
packages/ckeditor5-clipboard/tests/manual/copycut.js

@@ -36,6 +36,7 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 } )
 .then( editor => {
 	window.editor = editor;
+	const clipboard = editor.plugins.get( 'clipboard/clipboard' );
 
 	editor.editing.view.on( 'paste', ( evt, data ) => {
 		console.clear();
@@ -45,7 +46,7 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 	editor.editing.view.on( 'copy', onViewEvent, { priority: 'lowest' } );
 	editor.editing.view.on( 'cut', onViewEvent, { priority: 'lowest' } );
 
-	editor.editing.view.on( 'clipboardInput', onPipelineEvent );
+	clipboard.on( 'inputTransformation', onPipelineEvent );
 	editor.editing.view.on( 'clipboardOutput', ( evt, data ) => {
 		console.clear();
 		onPipelineEvent( evt, data );

+ 3 - 2
packages/ckeditor5-clipboard/tests/manual/pasting.js

@@ -36,6 +36,7 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 } )
 .then( editor => {
 	window.editor = editor;
+	const clipboard = editor.plugins.get( 'clipboard/clipboard' );
 
 	editor.editing.view.on( 'paste', ( evt, data ) => {
 		console.clear();
@@ -46,9 +47,9 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 		console.log( 'text/plain\n', data.dataTransfer.getData( 'text/plain' ) );
 	} );
 
-	editor.editing.view.on( 'clipboardInput', ( evt, data ) => {
+	clipboard.on( 'inputTransformation', ( evt, data ) => {
 		console.log( '----- clipboardInput -----' );
-		console.log( 'stringify( data.content )\n', stringifyView( data.content ) );
+		console.log( 'stringify( data.dataTransfer )\n', stringifyView( data.content ) );
 	} );
 } )
 .catch( err => {