Skip to content

Commit

Permalink
Merge pull request #9129 from ckeditor/i/2664-dnd
Browse files Browse the repository at this point in the history
Feature (clipboard): Implemented the basic content drag and drop support. Closes #9128.

Feature (clipboard): The `contentInsertion` event is fired from `ClipboardPipeline` to enable customization of content insertion (see #9128).

Other (clipboard): The paste as plain text feature was extracted to the dedicated `PastePlainText` plugin (see #9128).

Other (engine): The `mouseup` event is fired by the `MouseObserver` (see #9128).

Other (table): The `mouseup` event is no longer fired by the `MouseEventsObserver` from the ckeditor5-table package (now handled by `MouseObserver`) (see #9128).

Internal (code-block, image, link, media-embed, paste-from-office): Updated to the changes in clipboard pipeline (see #9128).

Internal (widget): The `mousedown` event is no longer stopped from the default handling to enable content dragging (see #9128).

BREAKING CHANGE (clipboard): The `inputTransformation` event is no longer fired by the `Clipboard` plugin, now the `ClipboardPipeline` plugin is responsible for firing that event (see #9128).

BREAKING CHANGE (clipboard): The `clipboardInput` and `inputTransformation` events should not be fired or stopped in the feature code. The `data.content` property should be assigned to override the default content instead. You can stop this event only if you want to completely disable pasting/dropping of some content \[TODO migration guide link\] (see #9128).
  • Loading branch information
oleq authored Mar 11, 2021
2 parents 01f695a + 375ccba commit 8461da5
Show file tree
Hide file tree
Showing 70 changed files with 5,037 additions and 1,395 deletions.
6 changes: 3 additions & 3 deletions packages/ckeditor5-ckfinder/tests/ckfindercommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
import ImageUploadEditing from '@ckeditor/ckeditor5-image/src/imageupload/imageuploadediting';
import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import ClipboardPipeline from '@ckeditor/ckeditor5-clipboard/src/clipboardpipeline';

import CKFinderCommand from '../src/ckfindercommand';
import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
Expand All @@ -26,7 +26,7 @@ describe( 'CKFinderCommand', () => {
beforeEach( () => {
return VirtualTestEditor
.create( {
plugins: [ Paragraph, ImageEditing, ImageUploadEditing, LinkEditing, Notification, Clipboard ]
plugins: [ Paragraph, ImageEditing, ImageUploadEditing, LinkEditing, Notification, ClipboardPipeline ]
} )
.then( newEditor => {
editor = newEditor;
Expand Down Expand Up @@ -256,7 +256,7 @@ describe( 'CKFinderCommand', () => {

return VirtualTestEditor
.create( {
plugins: [ Paragraph, ImageEditing, ImageUploadEditing, LinkEditing, Notification, Clipboard ],
plugins: [ Paragraph, ImageEditing, ImageUploadEditing, LinkEditing, Notification, ClipboardPipeline ],
language: 'pl'
} )
.then( newEditor => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ order: 30

# Pasting plain text

The plain text pasting feature is implemented by the {@link module:clipboard/clipboard~PastePlainText} plugin which is a part of the {@link module:clipboard/clipboard~Clipboard} plugin.
The plain text pasting feature is implemented by the {@link module:clipboard/pasteplaintext~PastePlainText} plugin which is a part of the {@link module:clipboard/clipboard~Clipboard} plugin.

It detects the <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke during the paste and causes the pasted text to inherit the styles of the content it was pasted into. In this sense, the feature can also be described as "pasting without formatting" &mdash; the source formatting of the pasted text gets replaced with the target formatting of the text it was pasted into.

Expand Down Expand Up @@ -51,7 +51,7 @@ ClassicEditor
.catch( ... );
```

The {@link module:clipboard/clipboard~PastePlainText `PastePlainText`} plugin will activate along with the clipboard plugin.
The {@link module:clipboard/pasteplaintext~PastePlainText `PastePlainText`} plugin will activate along with the clipboard plugin.

## Support for other applications

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ When the user pastes or drops content into the editor, the browser fires an even

1. {@link module:clipboard/clipboardobserver~ClipboardObserver} turns that event into a synthetic {@link module:engine/view/document~Document#event:paste `view.Document#paste`} or {@link module:engine/view/document~Document#event:drop `view.Document#drop`}.
2. Since the content to be inserted by both actions (paste and drop) should usually be processed in the same way and both actions have a very simillar effect, both events are turned into a single {@link module:engine/view/document~Document#event:clipboardInput `view.Document#clipboardInput`} event for easier handling.
3. Next, the clipboard feature listens to the `view.Document#clipboardInput` event, retrieves and pre-processes the `text/html` or `text/plain` content which it finds in the {@link module:clipboard/datatransfer~DataTransfer event's `dataTransfer`} and fires the {@link module:clipboard/clipboard~Clipboard#event:inputTransformation `Clipboard#inputTransformation`} event with the retrieved content.
3. Next, the clipboard feature listens to the `view.Document#clipboardInput` event, retrieves and pre-processes the `text/html` or `text/plain` content which it finds in the {@link module:clipboard/datatransfer~DataTransfer event's `dataTransfer`} and fires the {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation `ClipboardPipeline#inputTransformation`} event with the retrieved content.
4. Finally, the clipboard feature listens to the `Clipboard#inputTransformation` event, takes the processed content and {@link module:engine/model/model~Model#insertContent inserts} it into the editor.

The clipboard feature listens to the `view.Document#clipboardInput` and `Clipboard#inputTransformation` events using low priority listeners. This means that adding a normal listener and calling `evt.stop()` allows overriding the behavior implemented by the clipboard feature. It is a similar mechanism to DOM's `evt.preventDefault()` that lets you override the default browser behavior.
Expand Down Expand Up @@ -61,7 +61,7 @@ The {@link module:engine/view/document~Document#event:clipboardInput `view.Docum

### Processing input content

The {@link module:clipboard/clipboard~Clipboard#event:inputTransformation `view.Document#inputTransformation`} event lets you process the content which is going to be inserted into the editor.
The {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation `view.Document#inputTransformation`} event lets you process the content which is going to be inserted into the editor.

The default action is to {@link module:engine/model/model~Model#insertContent insert} the content (`data.content`, represented by a {@link module:engine/view/documentfragment~DocumentFragment}) to the editor if the data is not empty.

Expand All @@ -70,7 +70,7 @@ At this stage the pasted content can be processed by the features. For example,
```js
const writer = new UpcastWriter( editor.editing.view.document );

editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', ( evt, data ) => {
editor.plugins.get( 'ClipboardPipeline' ).on( 'inputTransformation', ( evt, data ) => {
if ( data.content.childCount == 1 && isUrlText( data.content.getChild( 0 ) ) ) {
const linkUrl = data.content.getChild( 0 ).data;

Expand All @@ -88,7 +88,7 @@ editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', ( evt, data ) => {
The default action (inserting the content into the editor) is performed by a low priority listener, so it can be overridden by a normal one. With the `lowest` priority you can also execute actions after the content was already inserted.

```js
editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', ( evt, data ) => {
editor.plugins.get( 'ClipboardPipeline' ).on( 'inputTransformation', ( evt, data ) => {
console.log( 'Content was inserted.' );
}, { priority: 'lowest' } );
```
Expand Down Expand Up @@ -128,7 +128,7 @@ class PastePlainText extends Plugin {
editor.commands.add( 'pastePlainText', new PastePlainTextCommand( editor ) );

// Logic responsible for converting HTML to plain text.
const clipboardPlugin = editor.plugins.get( 'Clipboard' );
const clipboardPlugin = editor.plugins.get( 'ClipboardPipeline' );
const command = editor.commands.get( 'pastePlainText' );
const editingView = editor.editing.view;

Expand Down
18 changes: 16 additions & 2 deletions packages/ckeditor5-clipboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@
"dependencies": {
"@ckeditor/ckeditor5-core": "^26.0.0",
"@ckeditor/ckeditor5-engine": "^26.0.0",
"@ckeditor/ckeditor5-utils": "^26.0.0"
"@ckeditor/ckeditor5-utils": "^26.0.0",
"@ckeditor/ckeditor5-widget": "^26.0.0",
"lodash-es": "^4.17.11"
},
"devDependencies": {
"@ckeditor/ckeditor5-alignment": "^26.0.0",
"@ckeditor/ckeditor5-basic-styles": "^26.0.0",
"@ckeditor/ckeditor5-block-quote": "^26.0.0",
"@ckeditor/ckeditor5-cloud-services": "^26.0.0",
"@ckeditor/ckeditor5-code-block": "^26.0.0",
"@ckeditor/ckeditor5-easy-image": "^26.0.0",
"@ckeditor/ckeditor5-editor-classic": "^26.0.0",
"@ckeditor/ckeditor5-enter": "^26.0.0",
"@ckeditor/ckeditor5-horizontal-line": "^26.0.0",
"@ckeditor/ckeditor5-image": "^26.0.0",
"@ckeditor/ckeditor5-link": "^26.0.0",
"@ckeditor/ckeditor5-paragraph": "^26.0.0"
"@ckeditor/ckeditor5-page-break": "^26.0.0",
"@ckeditor/ckeditor5-paragraph": "^26.0.0",
"@ckeditor/ckeditor5-paste-from-office": "^26.0.0",
"@ckeditor/ckeditor5-remove-format": "^26.0.0",
"@ckeditor/ckeditor5-table": "^26.0.0",
"@ckeditor/ckeditor5-typing": "^26.0.0"
},
"engines": {
"node": ">=12.0.0",
Expand Down
231 changes: 10 additions & 221 deletions packages/ckeditor5-clipboard/src/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,21 @@
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PastePlainText from './pasteplaintext';

import ClipboardObserver from './clipboardobserver';

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

import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
import ClipboardPipeline from './clipboardpipeline';
import DragDrop from './dragdrop';
import PastePlainText from './pasteplaintext';

/**
* The clipboard feature. It is responsible for intercepting the `paste` and `drop` events and
* passing the pasted content through the clipboard pipeline in order to insert it into the editor's content.
* It also handles the `cut` and `copy` events to fill the native clipboard with serialized editor's data.
* The clipboard feature.
*
* Read more about the clipboard integration in {@glink framework/guides/deep-dive/clipboard "Clipboard" deep dive} guide.
*
* This is a "glue" plugin which loads the following plugins:
* * {@link module:clipboard/clipboardpipeline~ClipboardPipeline}
* * {@link module:clipboard/dragdrop~DragDrop}
* * {@link module:clipboard/pasteplaintext~PastePlainText}
*
* @extends module:core/plugin~Plugin
*/
export default class Clipboard extends Plugin {
Expand All @@ -39,215 +37,6 @@ export default class Clipboard extends Plugin {
* @inheritDoc
*/
static get requires() {
return [ PastePlainText ];
return [ ClipboardPipeline, DragDrop, PastePlainText ];
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const modelDocument = editor.model.document;
const view = editor.editing.view;
const viewDocument = view.document;

view.addObserver( ClipboardObserver );

// The clipboard paste pipeline.

// Pasting and dropping is disabled when editor is read-only.
// See: https://github.com/ckeditor/ckeditor5-clipboard/issues/26.
this.listenTo( viewDocument, 'clipboardInput', evt => {
if ( editor.isReadOnly ) {
evt.stop();
}
}, { priority: 'highest' } );

this.listenTo( viewDocument, 'clipboardInput', ( 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.editor.data.htmlProcessor.toView( content );

const eventInfo = new EventInfo( this, 'inputTransformation' );
this.fire( eventInfo, {
content,
dataTransfer,
asPlainText: data.asPlainText
} );

// If CKEditor handled the input, do not bubble the original event any further.
// This helps external integrations recognize that fact and act accordingly.
// https://github.com/ckeditor/ckeditor5-upload/issues/92
if ( eventInfo.stop.called ) {
evt.stop();
}

view.scrollToTheSelection();
}, { priority: 'low' } );

this.listenTo( this, 'inputTransformation', ( evt, data ) => {
if ( !data.content.isEmpty ) {
const dataController = this.editor.data;
const model = this.editor.model;

// Convert the pasted content to a model document fragment.
// The 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' );

if ( modelFragment.childCount == 0 ) {
return;
}

model.change( writer => {
const selection = model.document.selection;

// Plain text can be determined based on event flag (#7799) or auto-detection (#1006). If detected,
// preserve selection attributes on pasted items.
if ( data.asPlainText || isPlainTextFragment( modelFragment, model.schema ) ) {
// Formatting attributes should be preserved.
const textAttributes = Array.from( selection.getAttributes() )
.filter( ( [ key ] ) => model.schema.getAttributeProperties( key ).isFormatting );

if ( !selection.isCollapsed ) {
model.deleteContent( selection, { doNotAutoparagraph: true } );
}

// Also preserve other attributes if they survived the content deletion (because they were not fully selected).
// For example linkHref is not a formatting attribute but it should be preserved if pasted text was in the middle
// of a link.
textAttributes.push( ...selection.getAttributes() );

const range = writer.createRangeIn( modelFragment );

for ( const item of range.getItems() ) {
if ( item.is( '$text' ) || item.is( '$textProxy' ) ) {
writer.setAttributes( textAttributes, item );
}
}
}

model.insertContent( modelFragment );
} );

evt.stop();
}
}, { priority: 'low' } );

// The clipboard copy/cut pipeline.

function onCopyCut( evt, data ) {
const dataTransfer = data.dataTransfer;

data.preventDefault();

const content = editor.data.toView( editor.model.getSelectedContent( modelDocument.selection ) );

viewDocument.fire( 'clipboardOutput', { dataTransfer, content, method: evt.name } );
}

this.listenTo( viewDocument, 'copy', onCopyCut, { priority: 'low' } );
this.listenTo( viewDocument, 'cut', ( evt, data ) => {
// Cutting is disabled when editor is read-only.
// See: https://github.com/ckeditor/ckeditor5-clipboard/issues/26.
if ( editor.isReadOnly ) {
data.preventDefault();
} else {
onCopyCut( evt, data );
}
}, { priority: 'low' } );

this.listenTo( viewDocument, 'clipboardOutput', ( evt, data ) => {
if ( !data.content.isEmpty ) {
data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) );
data.dataTransfer.setData( 'text/plain', viewToPlainText( data.content ) );
}

if ( data.method == 'cut' ) {
editor.model.deleteContent( modelDocument.selection );
}
}, { priority: 'low' } );
}
}

/**
* Fired with a `content` and `dataTransfer` objects. The `content` which comes from the clipboard (was pasted or dropped)
* should be processed in order to be inserted into the editor. The `dataTransfer` object is available
* in case the transformation functions need access to raw clipboard data.
*
* It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline "clipboard input pipeline"}.
*
* @see module:clipboard/clipboardobserver~ClipboardObserver
* @see module:clipboard/clipboard~Clipboard
* @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
* {@glink framework/guides/deep-dive/clipboard "Clipboard" deep dive}.
* @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer Data transfer instance.
* @param {Boolean} data.asPlainText If set to `true`, the content is pasted as plain text.
*/

/**
* Fired on {@link module:engine/view/document~Document#event:copy} and {@link module:engine/view/document~Document#event:cut}
* with a copy of selected content. The content can be processed before it ends up in the clipboard.
*
* It is a part of the {@glink framework/guides/deep-dive/clipboard#output-pipeline "clipboard output pipeline"}.
*
* @see module:clipboard/clipboardobserver~ClipboardObserver
* @see module:clipboard/clipboard~Clipboard
* @event module:engine/view/document~Document#event:clipboardOutput
* @param {module:clipboard/clipboard~ClipboardOutputEventData} data Event data.
*/

/**
* The value of the {@link module:engine/view/document~Document#event:clipboardOutput} event.
*
* @class module:clipboard/clipboard~ClipboardOutputEventData
*/

/**
* Data transfer instance.
*
* @readonly
* @member {module:clipboard/datatransfer~DataTransfer} module:clipboard/clipboard~ClipboardOutputEventData#dataTransfer
*/

/**
* Content to be put into the clipboard. It can be modified by the event listeners.
* Read more about the clipboard pipelines in {@glink framework/guides/deep-dive/clipboard "Clipboard" deep dive}.
*
* @member {module:engine/view/documentfragment~DocumentFragment} module:clipboard/clipboard~ClipboardOutputEventData#content
*/

/**
* Whether the event was triggered by a copy or cut operation.
*
* @member {'copy'|'cut'} module:clipboard/clipboard~ClipboardOutputEventData#method
*/

// Returns true if specified `documentFragment` represents a plain text.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function isPlainTextFragment( documentFragment, schema ) {
if ( documentFragment.childCount > 1 ) {
return false;
}

const child = documentFragment.getChild( 0 );

if ( schema.isObject( child ) ) {
return false;
}

return [ ...child.getAttributeKeys() ].length == 0;
}
Loading

0 comments on commit 8461da5

Please sign in to comment.