From 6c6702bb60361baf46e278d556076dc1fe952421 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 14 Apr 2017 15:59:27 +0200 Subject: [PATCH 1/2] Added: text/plain support for copy/cut. --- src/clipboard.js | 2 + src/utils/viewtoplaintext.js | 53 ++++++++++++++++++++ tests/clipboard.js | 95 ++++++++++++++++++++++++++++++++++-- tests/manual/copycut.md | 3 +- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/utils/viewtoplaintext.js diff --git a/src/clipboard.js b/src/clipboard.js index 8ee98c6..8b4c887 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -13,6 +13,7 @@ import ClipboardObserver from './clipboardobserver'; import plainTextToHtml from './utils/plaintexttohtml'; import normalizeClipboardHtml from './utils/normalizeclipboarddata'; +import viewToPlainText from './utils/viewtoplaintext.js'; import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor'; @@ -179,6 +180,7 @@ export default class Clipboard extends Plugin { this.listenTo( editingView, 'clipboardOutput', ( evt, data ) => { if ( !data.content.isEmpty ) { data.dataTransfer.setData( 'text/html', this._htmlDataProcessor.toData( data.content ) ); + data.dataTransfer.setData( 'text/plain', viewToPlainText( data.content ) ); } if ( data.method == 'cut' ) { diff --git a/src/utils/viewtoplaintext.js b/src/utils/viewtoplaintext.js new file mode 100644 index 0000000..c162968 --- /dev/null +++ b/src/utils/viewtoplaintext.js @@ -0,0 +1,53 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module clipboard/utils/viewtoplaintext + */ + +/** + * Deeply converts {@link module:engine/model/view/item view item} to plain text. + * + * @param {module:engine/model/view/item} viewItem View item to convert. + * @returns {String} Plain text representation of `viewItem`. + */ +export default function viewToPlainText( viewItem ) { + let text = ''; + + if ( viewItem.is( 'text' ) || viewItem.is( 'textProxy' ) ) { + // If item is `Text` or `TextProxy` simple take its text data. + text = viewItem.data; + } else if ( viewItem.is( 'img' ) && viewItem.hasAttribute( 'alt' ) ) { + // Special case for images - use alt attribute if it is provided. + text = viewItem.getAttribute( 'alt' ); + } else { + // Other elements are document fragments, attribute elements or container elements. + // They don't have their own text value, so convert their children. + let prev = null; + + for ( let child of viewItem.getChildren() ) { + const childText = viewToPlainText( child ); + + // Separate container element children with one or more new-line characters. + if ( prev && ( prev.is( 'containerElement' ) || child.is( 'containerElement' ) ) ) { + if ( smallPaddingElements.includes( prev.name ) || smallPaddingElements.includes( child.name ) ) { + text += '\n'; + } else { + text += '\n\n'; + } + } + + text += childText; + prev = child; + } + } + + return text; +} + +// Elements which should not have empty-line padding. +// Most `view.ContainerElement` want to be separate by new-line, but some are creating one structure +// together (like `
  • `) so it is better to separate them by only one "\n". +const smallPaddingElements = [ 'figcaption', 'li' ]; diff --git a/tests/clipboard.js b/tests/clipboard.js index be41355..53006ff 100644 --- a/tests/clipboard.js +++ b/tests/clipboard.js @@ -9,7 +9,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ClipboardObserver from '../src/clipboardobserver'; -import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { + stringify as stringifyView, + parse as parseView +} from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { stringify as stringifyModel, setData as setModelData, @@ -287,16 +290,98 @@ describe( 'Clipboard feature', () => { it( 'sets clipboard HTML data', () => { const dataTransferMock = createDataTransfer(); - setModelData( editor.document, 'f[o]o' ); + const input = + '
    ' + + '

    foo

    ' + + '

    bar

    ' + + '
    ' + + '' + + '

    foobar

    ' + + '
      ' + + '
    1. ol item
    2. ' + + '
    3. ol item
    4. ' + + '
    ' + + '
    ' + + 'image foo' + + '
    caption
    ' + + '
    '; + + const output = + '
    ' + + '

    foo

    ' + + '

    bar

    ' + + '
    ' + + '' + + '

    foobar

    ' + + '
      ' + + '
    1. ol item
    2. ' + + '
    3. ol item
    4. ' + + '
    ' + + '
    ' + + 'image foo' + // Weird attributes ordering behavior + no closing "/>". + '
    caption
    ' + + '
    '; editingView.fire( 'clipboardOutput', { dataTransfer: dataTransferMock, - content: new ViewDocumentFragment( [ new ViewText( 'abc' ) ] ), + content: parseView( input ), + method: 'copy' + } ); + + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( output ); + } ); + + it( 'sets clipboard plain text data', () => { + const dataTransferMock = createDataTransfer(); + + const input = + '' + + 'foo' + + 'bar' + + '' + + '' + + 'ul item' + + 'ul item' + + '' + + 'foobar' + + '' + + 'ol item' + + 'ol item' + + '' + + '' + + 'image foo' + + 'caption' + + ''; + + const output = + 'foo\n' + + '\n' + + 'bar\n' + + '\n' + + 'ul item\n' + + 'ul item\n' + + '\n' + + 'foobar\n' + + '\n' + + 'ol item\n' + + 'ol item\n' + + '\n' + + 'image foo\n' + + 'caption'; + + editingView.fire( 'clipboardOutput', { + dataTransfer: dataTransferMock, + content: parseView( input ), method: 'copy' } ); - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'abc' ); - expect( getModelData( editor.document ) ).to.equal( 'f[o]o' ); + expect( dataTransferMock.getData( 'text/plain' ) ).to.equal( output ); } ); it( 'does not set clipboard HTML data if content is empty', () => { diff --git a/tests/manual/copycut.md b/tests/manual/copycut.md index f20a08e..f3548bc 100644 --- a/tests/manual/copycut.md +++ b/tests/manual/copycut.md @@ -2,4 +2,5 @@ Play with copy and cut. Paste copied content. -Compare the results with the native editable. Don't expect that they behave identically. +Compare the results with the native editable. Don't expect that they behave identically. Check plain text pasting +(for example paste editor content to code editor). From e20ab94e4485d581b3c99667ca7588dab41daf69 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Thu, 20 Apr 2017 10:11:04 +0200 Subject: [PATCH 2/2] Tests: added tests for viewToPlainText. --- tests/utils/viewtoplaintext.js | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/utils/viewtoplaintext.js diff --git a/tests/utils/viewtoplaintext.js b/tests/utils/viewtoplaintext.js new file mode 100644 index 0000000..8a8d3e9 --- /dev/null +++ b/tests/utils/viewtoplaintext.js @@ -0,0 +1,72 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import viewToPlainText from '../../src/utils/viewtoplaintext'; + +import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +describe( 'viewToPlainText', () => { + function test( viewString, expectedText ) { + const view = parseView( viewString ); + const text = viewToPlainText( view ); + + expect( text ).to.equal( expectedText ); + } + + it( 'should output text contents of given view', () => { + test( + 'FooBarXyz', + 'FooBarXyz' + ); + } ); + + it( 'should put empty line between container elements', () => { + test( + 'Header' + + 'Foo' + + 'Bar' + + 'Abc' + + 'Xyz', + + 'Header\n\nFoo\n\nBar\n\nAbc\n\nXyz' + ); + } ); + + it( 'should output alt attribute of image elements', () => { + test( + 'Foo' + + 'Alt', + + 'Foo\n\nAlt' + ); + } ); + + it( 'should not put empty line after li (if not needed)', () => { + test( + 'Foo' + + '' + + 'A' + + 'B' + + 'C' + + '' + + 'Bar', + + 'Foo\n\nA\nB\nC\n\nBar' + ); + } ); + + it( 'should not put empty line before/after figcaption (if not needed)', () => { + test( + 'Foo' + + '' + + 'Alt' + + 'Caption' + + '' + + 'Bar', + + 'Foo\n\nAlt\nCaption\n\nBar' + ); + } ); +} );