Skip to content

Commit

Permalink
Merge pull request #7886 from ckeditor/i/7799
Browse files Browse the repository at this point in the history
Feature: Introduced the `PastePlainText` feature that detects pasting with <kbd>ctrl/cmd</kbd> + <kbd>shift</kbd> + <kbd>ctrl/v</kbd> keystroke. Closes #7799.
  • Loading branch information
pomek committed Sep 11, 2020
2 parents 0f18cab + 254f46e commit ab7bce9
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 6 deletions.
36 changes: 30 additions & 6 deletions packages/ckeditor5-clipboard/src/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

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

import ClipboardObserver from './clipboardobserver';

Expand Down Expand Up @@ -35,6 +36,13 @@ export default class Clipboard extends Plugin {
return 'Clipboard';
}

/**
* @inheritDoc
*/
static get requires() {
return [ PastePlainText ];
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -77,7 +85,11 @@ export default class Clipboard extends Plugin {
content = this._htmlDataProcessor.toView( content );

const eventInfo = new EventInfo( this, 'inputTransformation' );
this.fire( eventInfo, { content, dataTransfer } );
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.
Expand All @@ -103,16 +115,27 @@ export default class Clipboard extends Plugin {
return;
}

// While pasting plain text, apply selection attributes on the text.
if ( isPlainText( modelFragment ) ) {
const node = modelFragment.getChild( 0 );
// 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 ) ) {
// Consider only formatting attributes.
const textAttributes = new Map( Array.from( modelDocument.selection.getAttributes() ).filter(
keyValuePair => editor.model.schema.getAttributeProperties( keyValuePair[ 0 ] ).isFormatting
) );

model.change( writer => {
writer.setAttributes( modelDocument.selection.getAttributes(), node );
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' } );
Expand Down Expand Up @@ -168,6 +191,7 @@ export default class Clipboard extends Plugin {
* 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` content is pasted as plain text.
*/

/**
Expand Down Expand Up @@ -212,7 +236,7 @@ export default class Clipboard extends Plugin {
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
// @returns {Boolean}
function isPlainText( documentFragment ) {
function isPlainTextFragment( documentFragment ) {
if ( documentFragment.childCount > 1 ) {
return false;
}
Expand Down
49 changes: 49 additions & 0 deletions packages/ckeditor5-clipboard/src/pasteplaintext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module clipboard/clipboard
*/

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

import ClipboardObserver from './clipboardobserver';

/**
* The plugin detects user intentions for pasting plain text.
*
* For example, it detects <kbd>ctrl/cmd</kbd> + <kbd>shift</kbd> + <kbd>ctrl/v</kbd> keystroke.
*
* @extends module:core/plugin~Plugin
*/
export default class PastePlainText extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'PastePlainText';
}

/**
* @inheritDoc
*/
init() {
const view = this.editor.editing.view;
const viewDocument = view.document;
let shiftPressed = false;

view.addObserver( ClipboardObserver );

this.listenTo( viewDocument, 'keydown', ( evt, data ) => {
shiftPressed = data.shiftKey;
} );

this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
if ( shiftPressed ) {
data.asPlainText = true;
}
}, { priority: 'high' } );
}
}
93 changes: 93 additions & 0 deletions packages/ckeditor5-clipboard/tests/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,19 @@ describe( 'Clipboard feature', () => {
model = editor.model;

model.schema.extend( '$text', { allowAttributes: 'bold' } );
model.schema.extend( '$text', { allowAttributes: 'test' } );

editor.model.schema.setAttributeProperties( 'bold', { isFormatting: true } );

model.schema.register( 'softBreak', {
allowWhere: '$text',
isInline: true
} );
editor.conversion.for( 'upcast' )
.elementToElement( {
model: 'softBreak',
view: 'br'
} );
} );

it( 'should inherit selection attributes (collapsed selection)', () => {
Expand Down Expand Up @@ -451,6 +464,86 @@ describe( 'Clipboard feature', () => {

expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
} );

it( 'should inherit selection attributes with data.asPlainText switch set', () => {
setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );

const dataTransferMock = createDataTransfer( {
'text/html': 'foo',
'text/plain': 'foo'
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock,
asPlainText: true,
stopPropagation() {},
preventDefault() {}
} );

expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
} );

it( 'should discard selection attributes with data.asPlainText switch set to false', () => {
setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );

const dataTransferMock = createDataTransfer( {
'text/html': 'foo<br>bar',
'text/plain': 'foo\nbar'
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock,
asPlainText: false,
stopPropagation() {},
preventDefault() {}
} );

expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded </$text>' +
'foo<softBreak></softBreak>bar[]' +
'<$text bold="true">text.</$text></paragraph>' );
} );

it( 'should work if the insertContent event is cancelled', () => {
// (#7887).
setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );

const dataTransferMock = createDataTransfer( {
'text/html': 'foo',
'text/plain': 'foo'
} );

model.on( 'insertContent', event => {
event.stop();
}, { priority: 'high' } );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock,
asPlainText: false,
stopPropagation() {},
preventDefault() {}
} );

expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
} );

it( 'ignores non-formatting text attributes', () => {
setModelData( model, '<paragraph><$text test="true">Bolded []text.</$text></paragraph>' );

const dataTransferMock = createDataTransfer( {
'text/html': 'foo',
'text/plain': 'foo'
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock,
asPlainText: false,
stopPropagation() {},
preventDefault() {}
} );

expect( getModelData( model ) ).to.equal(
'<paragraph><$text test="true">Bolded </$text>foo[]<$text test="true">text.</$text></paragraph>' );
} );
} );

function createDataTransfer( data ) {
Expand Down
96 changes: 96 additions & 0 deletions packages/ckeditor5-clipboard/tests/pasteplaintext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';

import PastePlainText from '../src/pasteplaintext';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';

/* global document */

describe( 'PastePlainText', () => {
let editor, viewDocument;

beforeEach( () => {
return VirtualTestEditor
.create( {
plugins: [ PastePlainText, Paragraph ]
} )
.then( newEditor => {
editor = newEditor;
viewDocument = editor.editing.view.document;
} );
} );

it( 'marks clipboard input as plain text with shift pressed', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );

viewDocument.on( 'clipboardInput', ( event, data ) => {
expect( data.asPlainText ).to.be.true;

// No need for further execution.
event.stop();
} );

viewDocument.fire( 'keydown', {
keyCode: getCode( 'v' ),
shiftKey: true,
ctrlKey: true,
preventDefault: () => {},
domTarget: document.body
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock
} );
} );

it( 'ignores clipboard input as plain text when shift was released', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );

viewDocument.on( 'clipboardInput', ( event, data ) => {
expect( data.asPlainText ).to.be.undefined;

// No need for further execution.
event.stop();
} );

viewDocument.fire( 'keydown', {
keyCode: getCode( 'a' ),
shiftKey: true,
preventDefault: () => {},
domTarget: document.body
} );

viewDocument.fire( 'keyup', {
keyCode: getCode( 'a' ),
shiftKey: true,
preventDefault: () => {},
domTarget: document.body
} );

viewDocument.fire( 'keydown', {
keyCode: getCode( 'v' ),
shiftKey: false,
ctrlKey: true,
preventDefault: () => {},
domTarget: document.body
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock
} );
} );

function createDataTransfer( data ) {
return {
getData( type ) {
return data[ type ];
}
};
}
} );

0 comments on commit ab7bce9

Please sign in to comment.