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

Commit

Permalink
Merge pull request #18 from ckeditor/t/11
Browse files Browse the repository at this point in the history
Fix: Plain text data is now available in clipboard when copying or cutting editor contents. Closes #11.
  • Loading branch information
Reinmar committed Apr 20, 2017
2 parents e4e7e10 + e20ab94 commit 8a01e0f
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 6 deletions.
2 changes: 2 additions & 0 deletions src/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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' ) {
Expand Down
53 changes: 53 additions & 0 deletions src/utils/viewtoplaintext.js
Original file line number Diff line number Diff line change
@@ -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 `<li>`) so it is better to separate them by only one "\n".
const smallPaddingElements = [ 'figcaption', 'li' ];
95 changes: 90 additions & 5 deletions tests/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -287,16 +290,98 @@ describe( 'Clipboard feature', () => {
it( 'sets clipboard HTML data', () => {
const dataTransferMock = createDataTransfer();

setModelData( editor.document, '<paragraph>f[o]o</paragraph>' );
const input =
'<blockquote>' +
'<p>foo</p>' +
'<p>bar</p>' +
'</blockquote>' +
'<ul>' +
'<li>u<strong>l ite</strong>m</li>' +
'<li>ul item</li>' +
'</ul>' +
'<p>foobar</p>' +
'<ol>' +
'<li>o<a href="foo">l ite</a>m</li>' +
'<li>ol item</li>' +
'</ol>' +
'<figure>' +
'<img src="foo.jpg" alt="image foo" />' +
'<figcaption>caption</figcaption>' +
'</figure>';

const output =
'<blockquote>' +
'<p>foo</p>' +
'<p>bar</p>' +
'</blockquote>' +
'<ul>' +
'<li>u<strong>l ite</strong>m</li>' +
'<li>ul item</li>' +
'</ul>' +
'<p>foobar</p>' +
'<ol>' +
'<li>o<a href="foo">l ite</a>m</li>' +
'<li>ol item</li>' +
'</ol>' +
'<figure>' +
'<img alt="image foo" src="foo.jpg">' + // Weird attributes ordering behavior + no closing "/>".
'<figcaption>caption</figcaption>' +
'</figure>';

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 =
'<container:blockquote>' +
'<container:p>foo</container:p>' +
'<container:p>bar</container:p>' +
'</container:blockquote>' +
'<container:ul>' +
'<container:li>u<strong>l ite</strong>m</container:li>' +
'<container:li>ul item</container:li>' +
'</container:ul>' +
'<container:p>foobar</container:p>' +
'<container:ol>' +
'<container:li>o<a href="foo">l ite</a>m</container:li>' +
'<container:li>ol item</container:li>' +
'</container:ol>' +
'<container:figure>' +
'<img alt="image foo" src="foo.jpg" />' +
'<container:figcaption>caption</container:figcaption>' +
'</container:figure>';

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( '<paragraph>f[o]o</paragraph>' );
expect( dataTransferMock.getData( 'text/plain' ) ).to.equal( output );
} );

it( 'does not set clipboard HTML data if content is empty', () => {
Expand Down
3 changes: 2 additions & 1 deletion tests/manual/copycut.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
72 changes: 72 additions & 0 deletions tests/utils/viewtoplaintext.js
Original file line number Diff line number Diff line change
@@ -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(
'<container:p>Foo<strong>Bar</strong>Xyz</container:p>',
'FooBarXyz'
);
} );

it( 'should put empty line between container elements', () => {
test(
'<container:h1>Header</container:h1>' +
'<container:p>Foo</container:p>' +
'<container:p>Bar</container:p>' +
'Abc' +
'<container:div>Xyz</container:div>',

'Header\n\nFoo\n\nBar\n\nAbc\n\nXyz'
);
} );

it( 'should output alt attribute of image elements', () => {
test(
'<container:p>Foo</container:p>' +
'<img src="foo.jpg" alt="Alt" />',

'Foo\n\nAlt'
);
} );

it( 'should not put empty line after li (if not needed)', () => {
test(
'<container:p>Foo</container:p>' +
'<container:ul>' +
'<container:li>A</container:li>' +
'<container:li>B</container:li>' +
'<container:li>C</container:li>' +
'</container:ul>' +
'<container:p>Bar</container:p>',

'Foo\n\nA\nB\nC\n\nBar'
);
} );

it( 'should not put empty line before/after figcaption (if not needed)', () => {
test(
'<container:p>Foo</container:p>' +
'<container:figure>' +
'<img src="foo.jpg" alt="Alt" />' +
'<container:figcaption>Caption</container:figcaption>' +
'</container:figure>' +
'<container:p>Bar</container:p>',

'Foo\n\nAlt\nCaption\n\nBar'
);
} );
} );

0 comments on commit 8a01e0f

Please sign in to comment.