8
0
فهرست منبع

Merge pull request #7886 from ckeditor/i/7799

Feature: Introduced the `PastePlainText` feature that detects pasting with <kbd>ctrl/cmd</kbd> + <kbd>shift</kbd> + <kbd>ctrl/v</kbd> keystroke. Closes #7799.
Kamil Piechaczek 5 سال پیش
والد
کامیت
ab7bce94eb

+ 30 - 6
packages/ckeditor5-clipboard/src/clipboard.js

@@ -8,6 +8,7 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import PastePlainText from './pasteplaintext';
 
 import ClipboardObserver from './clipboardobserver';
 
@@ -35,6 +36,13 @@ export default class Clipboard extends Plugin {
 		return 'Clipboard';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ PastePlainText ];
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -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.
@@ -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' } );
@@ -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.
  */
 
 /**
@@ -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;
 	}

+ 49 - 0
packages/ckeditor5-clipboard/src/pasteplaintext.js

@@ -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 - 0
packages/ckeditor5-clipboard/tests/clipboard.js

@@ -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)', () => {
@@ -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 ) {

+ 96 - 0
packages/ckeditor5-clipboard/tests/pasteplaintext.js

@@ -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 ];
+			}
+		};
+	}
+} );