Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #17 from ckeditor/t/16
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Piotr Jasiun committed Apr 12, 2017
2 parents 7ac66f2 + 8d38cfc commit e4e7e10
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 80 deletions.
64 changes: 29 additions & 35 deletions src/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*
Expand All @@ -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;
*
Expand Down Expand Up @@ -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 = '';

Expand All @@ -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' );

Expand Down Expand Up @@ -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}
*/

/**
Expand Down
39 changes: 37 additions & 2 deletions src/clipboardobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
31 changes: 31 additions & 0 deletions src/datatransfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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() );
}
96 changes: 67 additions & 29 deletions tests/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Expand All @@ -29,6 +29,7 @@ describe( 'Clipboard feature', () => {
.then( ( newEditor ) => {
editor = newEditor;
editingView = editor.editing.view;
clipboardPlugin = editor.plugins.get( 'clipboard/clipboard' );
} );
} );

Expand All @@ -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();
} );

Expand Down
Loading

0 comments on commit e4e7e10

Please sign in to comment.