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

Initial implementation of the clipboard feature #4

Merged
merged 9 commits into from
Oct 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import Feature from '../core/feature.js';

import ClipboardObserver from './clipboardobserver.js';

import plainTextToHtml from './utils/plaintexttohtml.js';
import normalizeClipboardHtml from './utils/normalizeclipboarddata.js';

import HtmlDataProcessor from '../engine/dataprocessor/htmldataprocessor.js';

/**
* The clipboard feature. Currently, it's only responsible for intercepting the `paste` event and
* passing the pasted content through the clipboard pipeline.
*
* ## Clipboard Pipeline
*
* The feature creates the clipboard pipeline which allows for processing clipboard content
* 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 engine.view.Document#paste}
*
* The default action is to:
*
* 1. get HTML or plain text from the clipboard,
* 2. fire {@link engine.view.Document#clipboardInput} with the clipboard data parsed to
* a {@link engine.view.DocumentFragment view document fragment},
* 3. prevent the default action of the native `paste` event.
*
* This action is performed by a low priority listener, so it can be overridden by a normal one.
* You'd only need to do this when a deeper change in pasting behavior was needed. For example,
* a feature which wants to differently read data from the clipboard (the {@link clipboard.DataTransfer `DataTransfer`}).
* should plug a listener at this stage.
*
* ### On {@link engine.view.Document#clipboardInput}
*
* The default action is to insert the content (`data.content`, represented by a {@link engine.view.DocumentFragment})
* to an editor if the data is not empty.
*
* This action is performed by a low priority listener, so it can be overridden by a normal one.
*
* 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 ) => {
* if ( data.content.childCount == 1 && isUrlText( data.content.getChild( 0 ) ) ) {
* const linkUrl = data.content.getChild( 0 ).data;
*
* data.content = new ViewDocumentFragment( [
* ViewElement(
* 'a',
* { href: linkUrl },
* [ new ViewText( linkUrl ) ]
* )
* ] );
* }
* } );
*
* @memberOf clipboard
* @extends core.Feature
*/
export default class Clipboard extends Feature {
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const editingView = editor.editing.view;

/**
* Data processor used to convert pasted HTML to a view structure.
*
* @private
* @member {engine.dataProcessor.HtmlDataProcessor} clipboard.Clipboard#_htmlDataProcessor
*/
this._htmlDataProcessor = new HtmlDataProcessor();

editingView.addObserver( ClipboardObserver );

// The clipboard pipeline.

this.listenTo( editingView, 'paste', ( evt, data ) => {
const dataTransfer = data.dataTransfer;
let content = '';

if ( dataTransfer.getData( 'text/html' ) ) {
content = normalizeClipboardHtml( dataTransfer.getData( 'text/html' ) );
} else if ( dataTransfer.getData( 'text/plain' ) ) {
content = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
}

content = this._htmlDataProcessor.toView( content );

data.preventDefault();

editingView.fire( 'clipboardInput', { dataTransfer, content } );
}, { priority: 'low' } );

this.listenTo( editingView, 'clipboardInput', ( evt, data ) => {
if ( !data.content.isEmpty ) {
const doc = editor.document;

doc.enqueueChanges( () => {
this.editor.data.insert( data.content, doc.selection );
} );
}
}, { priority: 'low' } );
}
}

/**
* 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 clipboard.Clipboard "clipboard pipeline"}.
*
* @see clipboard.ClipboardObserver
* @see clipboard.Clipboard
* @event engine.view.Document#clipboardInput
* @param {engine.view.observer.ClipboardInputEventData} data Event data.
*/

/**
* The value of the {@link engine.view.Document#clipboardInput} event.
*
* @class engine.view.observer.ClipboardInputEventData
*/

/**
* Data transfer instance.
*
* @readonly
* @member {clipboard.DataTransfer} engine.view.observer.ClipboardEventData#dataTransfer
*/

/**
* Content to be inserted into the editor. It can be modified by the event listeners.
* Read more about the clipboard pipeline in {@link clipboard.Clipboard}.
*
* @member {engine.view.DocumentFragment} engine.view.observer.ClipboardEventData#content
*/
60 changes: 60 additions & 0 deletions src/clipboardobserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import DomEventObserver from '../engine/view/observer/domeventobserver.js';
import DataTransfer from './datatransfer.js';

/**
* {@link engine.view.Document#paste Paste} event observer.
*
* Note that this observer is not available by default. To make it available it needs to be added to {@link engine.view.Document}
* by the {@link engine.view.Document#addObserver} method.
*
* @memberOf clipboard
* @extends engine.view.observer.DomEventObserver
*/
export default class ClipboardObserver extends DomEventObserver {
constructor( doc ) {
super( doc );

this.domEventType = 'paste';
}

onDomEvent( domEvent ) {
this.fire( domEvent.type, domEvent, {
dataTransfer: new DataTransfer( domEvent.clipboardData )
} );
}
}

/**
* Fired when user pasted content into one of the editables.
*
* Introduced by {@link clipboard.ClipboardObserver}.
*
* Note that this event is not available by default. To make it available {@link clipboard.ClipboardObserver} needs to be added
* to {@link engine.view.Document} by the {@link engine.view.Document#addObserver} method.
* It's done by the {@link clipboard.Clipboard} feature. If it's not loaded, it must be done manually.
*
* @see clipboard.ClipboardObserver
* @event engine.view.Document#paste
* @param {engine.view.observer.ClipboardEventData} data Event data.
*/

/**
* The value of the {@link engine.view.Document#paste} event.
*
* In order to access clipboard data use {@link #dataTransfer}.
*
* @class engine.view.observer.ClipboardEventData
* @extends engine.view.observer.DomEventData
*/

/**
* Data transfer instance.
*
* @readonly
* @member {clipboard.DataTransfer} engine.view.observer.ClipboardEventData#dataTransfer
*/
33 changes: 33 additions & 0 deletions src/datatransfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* Facade over the native [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) object.
*
* @memberOf clipboard
*/
export default class DataTransfer {
constructor( nativeDataTransfer ) {
/**
* The native DataTransfer object.
*
* @private
* @member {DataTransfer} clipboard.DataTransfer#_native
*/
this._native = nativeDataTransfer;
}

/**
* Gets data from the data transfer by its mime type.
*
* dataTransfer.getData( 'text/plain' );
*
* @param {String} type The mime type. E.g. `text/html` or `text/plain`.
* @returns {String}
*/
getData( type ) {
return this._native.getData( type );
}
}
24 changes: 24 additions & 0 deletions src/utils/normalizeclipboarddata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* Removes some popular browser quirks out of the clipboard data (HTML).
*
* @function clipboard.utils.normalizeClipboardData
* @param {String} data The HTML data to normalize.
* @returns {String} Normalized HTML.
*/
export default function normalizeClipboardData( data ) {
return data
.replace( /<span class="Apple-converted-space">(\s+)<\/span>/g, ( fullMatch, spaces ) => {
// Handle the most popular and problematic case when even a single space becomes an nbsp;.
// Decode those to normal spaces. Read more in https://github.com/ckeditor/ckeditor5-clipboard/issues/2.
if ( spaces.length == 1 ) {
return ' ';
}

return spaces;
} );
}
37 changes: 37 additions & 0 deletions src/utils/plaintexttohtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* Converts plain text to its HTML-ized version.
*
* @function clipboard.utils.plainTextToHtml
* @param {String} text The plain text to convert.
* @returns {String} HTML generated from the plain text.
*/
export default function plainTextToHtml( text ) {
text = text
// Encode <>.
.replace( /</g, '&lt;' )
.replace( />/g, '&gt;' )
// Creates paragraphs for double line breaks and change single line breaks to spaces.
// In the future single line breaks may be converted into <br>s.
.replace( /\n\n/g, '</p><p>' )
.replace( /\n/g, ' ' )
// Preserve trailing spaces (only the first and last one – the rest is handled below).
.replace( /^\s/, '&nbsp;' )
.replace( /\s$/, '&nbsp;' )
// Preserve other subsequent spaces now.
.replace( /\s\s/g, ' &nbsp;' );

if ( text.indexOf( '</p><p>' ) > -1 ) {
// If we created paragraphs above, add the trailing ones.
text = `<p>${ text }</p>`;
}

// TODO:
// * What about '\nfoo' vs ' foo'?

return text;
}
Loading