diff --git a/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md b/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md index 7352fcbaedb..4ddfdef4351 100644 --- a/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md +++ b/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md @@ -231,15 +231,20 @@ The output pipeline is the equivalent of the input pipeline but for the copy and ```plaintext ┌──────────────────────┐ ┌──────────────────────┐ Retrieves the selected │ view.Document │ │ view.Document │ model.DocumentFragment - │ copy │ │ cut │ and converts it to - └───────────┬──────────┘ └───────────┬──────────┘ view.DocumentFragment. + │ copy │ │ cut │ and fires `outputTransformation` + └───────────┬──────────┘ └───────────┬──────────┘ event. │ │ └────────────────┌────────────────┘ │ - ┌─────────V────────┐ Processes view.DocumentFragment - │ view.Document │ to text/html and text/plain - │ clipboardOutput │ and stores results in data.dataTransfer. - └──────────────────┘ + ┌─────────────V────────────┐ Processes model.DocumentFragment + │ ClipboardPipeline │ and converts it to + │ outputTransformation │ view.DocumentFragment. + └──────────────────────────┘ + │ + ┌─────────────V────────────┐ Processes view.DocumentFragment + │ view.Document │ to text/html and text/plain + │ clipboardOutput │ and stores results in data.dataTransfer. + └──────────────────────────┘ ``` ### 1. On {@link module:engine/view/document~Document#event:copy `view.Document#copy`} and {@link module:engine/view/document~Document#event:cut `view.Document#cut`} @@ -248,9 +253,16 @@ The default action is to: 1. {@link module:engine/model/model~Model#getSelectedContent Get the selected content} from the editor. 1. Prevent the default action of the native `copy` or `cut` event. -1. Fire {@link module:engine/view/document~Document#event:clipboardOutput `view.Document#clipboardOutput`} with a clone of the selected content converted to a {@link module:engine/view/documentfragment~DocumentFragment view document fragment}. +1. Fire {@link module:engine/view/document~Document#event:outputTransformation `view.Document#outputTransformation`}` with a selected content represented by a {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. + +### 2. On {@link module:engine/view/document~Document#event:outputTransformation `view.Document#outputTransformation`} + +The default action is to: + +1. Processes `data.content` represented by a {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. +1. Fire {@link module:engine/view/document~Document#event:clipboardOutput `view.Document#clipboardOutput`} with a processed `data.content` converted to a {@link module:engine/view/documentfragment~DocumentFragment view document fragment}. -### 2. On {@link module:engine/view/document~Document#event:clipboardOutput `view.Document#clipboardOutput`} +### 3. On {@link module:engine/view/document~Document#event:clipboardOutput `view.Document#clipboardOutput`} The default action is to put the content (`data.content`, represented by a {@link module:engine/view/documentfragment~DocumentFragment}) to the clipboard as HTML. In case of the cut operation, the selected content is also deleted from the editor. diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 3a79d725459..ee071ee20fa 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -17,7 +17,9 @@ import type { DomEventData, Range, ViewDocumentFragment, - ViewRange + ViewRange, + Selection, + DocumentSelection } from '@ckeditor/ckeditor5-engine'; import ClipboardObserver, { @@ -60,11 +62,16 @@ import viewToPlainText from './utils/viewtoplaintext'; // // ┌──────────────────────┐ ┌──────────────────────┐ // │ view.Document │ │ view.Document │ Retrieves the selected model.DocumentFragment -// │ copy │ │ cut │ and converts it to view.DocumentFragment. +// │ copy │ │ cut │ and fires `outputTransformation` event. // └───────────┬──────────┘ └───────────┬──────────┘ // │ │ // └────────────────┌────────────────┘ // │ +// ┌───────────V───────────┐ +// │ ClipboardPipeline │ Processes model.DocumentFragment and converts it to +// │ outputTransformation │ view.DocumentFragment. +// └───────────┬───────────┘ +// │ // ┌─────────V────────┐ // │ view.Document │ Processes view.DocumentFragment to text/html and text/plain // │ clipboardOutput │ and stores the results in data.dataTransfer. @@ -153,6 +160,25 @@ export default class ClipboardPipeline extends Plugin { this._setupCopyCut(); } + /** + * Fires Clipboard `'outputTransformation'` event for given parameters. + * + * @internal + */ + public _fireOutputTransformationEvent( + dataTransfer: DataTransfer, + selection: Selection | DocumentSelection, + method: 'copy' | 'cut' | 'dragstart' + ): void { + const content = this.editor.model.getSelectedContent( selection ); + + this.fire( 'outputTransformation', { + dataTransfer, + content, + method + } ); + } + /** * The clipboard paste pipeline. */ @@ -257,13 +283,7 @@ export default class ClipboardPipeline extends Plugin { data.preventDefault(); - const content = editor.data.toView( editor.model.getSelectedContent( modelDocument.selection ) ); - - viewDocument.fire( 'clipboardOutput', { - dataTransfer, - content, - method: evt.name - } ); + this._fireOutputTransformationEvent( dataTransfer, modelDocument.selection, evt.name ); }; this.listenTo( viewDocument, 'copy', onCopyCut, { priority: 'low' } ); @@ -277,6 +297,16 @@ export default class ClipboardPipeline extends Plugin { } }, { priority: 'low' } ); + this.listenTo( this, 'outputTransformation', ( evt, data ) => { + const content = editor.data.toView( data.content ); + + viewDocument.fire( 'clipboardOutput', { + dataTransfer: data.dataTransfer, + content, + method: data.method + } ); + }, { priority: 'low' } ); + this.listenTo( viewDocument, 'clipboardOutput', ( evt, data ) => { if ( !data.content.isEmpty ) { data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) ); @@ -441,3 +471,41 @@ export interface ViewDocumentClipboardOutputEventData { */ method: 'copy' | 'cut' | 'dragstart'; } + +/** + * Fired on {@link module:engine/view/document~Document#event:copy}, {@link module:engine/view/document~Document#event:cut} + * and {@link module:engine/view/document~Document#event:dragstart}. The content can be processed before it ends up in the clipboard. + * + * It is a part of the {@glink framework/deep-dive/clipboard#output-pipeline clipboard output pipeline}. + * + * @eventName ~ClipboardPipeline##outputTransformation + * @param data The event data. + */ +export type ClipboardOutputTransformationEvent = { + name: 'outputTransformation'; + args: [ data: ClipboardOutputTransformationData ]; +}; + +/** + * The value of the 'outputTransformation' event. + */ +export interface ClipboardOutputTransformationData { + + /** + * The data transfer instance. + * + * @readonly + */ + dataTransfer: DataTransfer; + + /** + * Content to be put into the clipboard. It can be modified by the event listeners. + * Read more about the clipboard pipelines in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide. + */ + content: DocumentFragment; + + /** + * Whether the event was triggered by a copy or cut operation. + */ + method: 'copy' | 'cut' | 'dragstart'; +} diff --git a/packages/ckeditor5-clipboard/src/dragdrop.ts b/packages/ckeditor5-clipboard/src/dragdrop.ts index c046589ac78..f1943485a2b 100644 --- a/packages/ckeditor5-clipboard/src/dragdrop.ts +++ b/packages/ckeditor5-clipboard/src/dragdrop.ts @@ -43,8 +43,7 @@ import { } from '@ckeditor/ckeditor5-utils'; import ClipboardPipeline, { - type ClipboardContentInsertionEvent, - type ViewDocumentClipboardOutputEvent + type ClipboardContentInsertionEvent } from './clipboardpipeline'; import ClipboardObserver, { @@ -290,13 +289,9 @@ export default class DragDrop extends Plugin { data.dataTransfer.setData( 'application/ckeditor5-dragging-uid', this._draggingUid ); const draggedSelection = model.createSelection( this._draggedRange.toRange() ); - const content = editor.data.toView( model.getSelectedContent( draggedSelection ) ); + const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - viewDocument.fire( 'clipboardOutput', { - dataTransfer: data.dataTransfer, - content, - method: 'dragstart' - } ); + clipboardPipeline._fireOutputTransformationEvent( data.dataTransfer, draggedSelection, 'dragstart' ); const { dataTransfer, domTarget, domEvent } = data; const { clientX } = domEvent; diff --git a/packages/ckeditor5-clipboard/src/index.ts b/packages/ckeditor5-clipboard/src/index.ts index 51685ae30af..ac05573bb07 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -13,6 +13,8 @@ export { type ClipboardContentInsertionEvent, type ClipboardInputTransformationEvent, type ClipboardInputTransformationData, + type ClipboardOutputTransformationEvent, + type ClipboardOutputTransformationData, type ViewDocumentClipboardOutputEvent } from './clipboardpipeline'; diff --git a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js index c8439327316..dfa0e09b899 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -460,6 +460,29 @@ describe( 'ClipboardPipeline feature', () => { } ); describe( 'clipboard copy/cut pipeline', () => { + it( 'fires the outputTransformation event on the clipboardPlugin', done => { + const dataTransferMock = createDataTransfer(); + const preventDefaultSpy = sinon.spy(); + + setModelData( editor.model, 'a[bcde]f' ); + + clipboardPlugin.on( 'outputTransformation', ( evt, data ) => { + expect( preventDefaultSpy.calledOnce ).to.be.true; + + expect( data.method ).to.equal( 'copy' ); + expect( data.dataTransfer ).to.equal( dataTransferMock ); + expect( data.content ).is.instanceOf( ModelDocumentFragment ); + expect( stringifyModel( data.content ) ).to.equal( 'bcde' ); + + done(); + } ); + + viewDocument.fire( 'copy', { + dataTransfer: dataTransferMock, + preventDefault: preventDefaultSpy + } ); + } ); + it( 'fires clipboardOutput for copy with the selected content and correct method', done => { const dataTransferMock = createDataTransfer(); const preventDefaultSpy = sinon.spy(); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdroplists.html b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.html new file mode 100644 index 00000000000..e1d809a7c13 --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.html @@ -0,0 +1,23 @@ +

Classic Editor - document lists

+ +
+

+

+
    +
  • Creating new types of editors. You can create new editor types using the framework.
  • +
  • Writing your own features. New features are implemented using the framework.
  • +
+

+
    +
  • Customizing existing features. Changing the behavior or look of existing features can be + done + thanks to the framework’s capabilities.
  • +
+

+
+ +
+

+
diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js new file mode 100644 index 00000000000..729742a58d9 --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js @@ -0,0 +1,107 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Image, ImageCaption, ImageStyle, ImageToolbar } from '@ckeditor/ckeditor5-image'; +import { Indent } from '@ckeditor/ckeditor5-indent'; +import { Link } from '@ckeditor/ckeditor5-link'; +import { DocumentList, DocumentListProperties } from '@ckeditor/ckeditor5-list'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'; +import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace'; +import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; +import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; +import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; +import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption'; +import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize'; +import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; +import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; +import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; +import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage'; +import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; +import Mention from '@ckeditor/ckeditor5-mention/src/mention'; +import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; +import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; +import TextPartLanguage from '@ckeditor/ckeditor5-language/src/textpartlanguage'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; +import Style from '@ckeditor/ckeditor5-style/src/style'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#editor-classic-lists' ), { + plugins: [ + Essentials, Autoformat, BlockQuote, Bold, Heading, Image, ImageCaption, ImageStyle, ImageToolbar, Indent, Italic, Link, + DocumentList, Paragraph, Table, TableToolbar, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat, + FindAndReplace, FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight, + CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, + EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, + AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport + ], + toolbar: [ + 'heading', 'style', + '|', + 'removeFormat', 'bold', 'italic', 'strikethrough', 'underline', 'code', 'subscript', 'superscript', 'link', + '|', + 'highlight', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', + '|', + 'bulletedList', 'numberedList', + '|', + 'blockQuote', 'insertImage', 'insertTable', 'codeBlock', + '|', + 'htmlEmbed', + '|', + 'alignment', 'outdent', 'indent', + '|', + 'pageBreak', 'horizontalLine', + '|', + 'textPartLanguage', + '|', + 'sourceEditing', + '|', + 'undo', 'redo', 'findAndReplace' + ], + cloudServices: CS_CONFIG, + placeholder: 'Type the content here!', + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdroplists.md b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 6601ce84f30..862e6a055d9 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -19,7 +19,6 @@ import type { DowncastWriter, Element, Model, - ModelGetSelectedContentEvent, ModelInsertContentEvent, UpcastElementEvent, ViewDocumentTabEvent, @@ -69,6 +68,11 @@ import ListWalker, { ListBlocksIterable } from './utils/listwalker'; +import { + ClipboardPipeline, + type ClipboardOutputTransformationEvent +} from 'ckeditor5/src/clipboard'; + import '../../theme/documentlist.css'; import '../../theme/list.css'; @@ -106,7 +110,7 @@ export default class DocumentListEditing extends Plugin { * @inheritDoc */ public static get requires() { - return [ Enter, Delete, DocumentListUtils ] as const; + return [ Enter, Delete, DocumentListUtils, ClipboardPipeline ] as const; } /** @@ -537,6 +541,7 @@ export default class DocumentListEditing extends Plugin { */ private _setupClipboardIntegration() { const model = this.editor.model; + const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); this.listenTo( model, 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); @@ -567,13 +572,31 @@ export default class DocumentListEditing extends Plugin { // │ * bar] │ * bar │ // └─────────────────────┴───────────────────┘ // - // See https://github.com/ckeditor/ckeditor5/issues/11608. - this.listenTo( model, 'getSelectedContent', ( evt, [ selection ] ) => { - const isSingleListItemSelected = isSingleListItem( Array.from( selection.getSelectedBlocks() ) ); + // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969 + this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { + model.change( writer => { + // Remove last block if it's empty. + const allContentChildren = Array.from( data.content.getChildren() ); + const lastItem = allContentChildren[ allContentChildren.length - 1 ]; + + if ( allContentChildren.length > 1 && lastItem.is( 'element' ) && lastItem.isEmpty ) { + const contentChildrenExceptLastItem = allContentChildren.slice( 0, -1 ); + + if ( contentChildrenExceptLastItem.every( isListItemBlock ) ) { + writer.remove( lastItem ); + } + } - if ( isSingleListItemSelected ) { - model.change( writer => removeListAttributes( Array.from( evt.return!.getChildren() as any ), writer ) ); - } + // Copy/cut only content of a list item (for drag-drop move the whole list item). + if ( data.method == 'copy' || data.method == 'cut' ) { + const allChildren = Array.from( data.content.getChildren() ); + const isSingleListItemSelected = isSingleListItem( allChildren ); + + if ( isSingleListItemSelected ) { + removeListAttributes( allChildren as Array, writer ); + } + } + } ); } ); } } diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js index 35d6f5f8340..ceb75b2f0b3 100644 --- a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js +++ b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js @@ -28,12 +28,19 @@ import { stringify as stringifyModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { + parse as parseView, + stringify as stringifyView +} from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +import { + LiveRange +} from '@ckeditor/ckeditor5-engine'; import stubUid from '../_utils/uid'; describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { - let element, editor, model, modelDoc, modelRoot, view; + let element, editor, model, modelDoc, modelRoot, view, clipboard; testUtils.createSinonSandbox(); @@ -58,6 +65,8 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { allowAttributes: 'foo' } ); + clipboard = editor.plugins.get( 'ClipboardPipeline' ); + // Stub `view.scrollToTheSelection` as it will fail on VirtualTestEditor without DOM. sinon.stub( view, 'scrollToTheSelection' ).callsFake( () => { } ); stubUid(); @@ -126,10 +135,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { '* []' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; + } ); - expect( modelFragment.childCount ).to.equal( 1 ); - expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should return an object stripped of list attributes, if that object was selected as a middle list item block', () => { @@ -139,10 +150,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ' bar' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; + } ); - expect( modelFragment.childCount ).to.equal( 1 ); - expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should strip other list attributes', () => { @@ -150,10 +163,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { '* []' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; + } ); - expect( modelFragment.childCount ).to.equal( 1 ); - expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should return nodes stripped of list attributes, if more than a single block of the same item was selected', () => { @@ -163,10 +178,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ' B]az' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 3 ); + expect( Array.from( data.content.getChildren() ).some( isListItemBlock ) ).to.be.false; + } ); - expect( modelFragment.childCount ).to.equal( 3 ); - expect( Array.from( modelFragment.getChildren() ).some( isListItemBlock ) ).to.be.false; + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should return just a text, if a list item block was partially selected', () => { @@ -235,10 +252,14 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { // [* Foo] // // Note: It is impossible to set a document selection like this because the postfixer will normalize it to * [Foo]. - const modelFragment = model.getSelectedContent( model.createSelection( model.document.getRoot(), 'in' ) ); - expect( modelFragment.childCount ).to.equal( 1 ); - expect( Array.from( modelFragment.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; + view.document.on( 'outputTransformation', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( Array.from( data.content.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; + } ); + + clipboard._fireOutputTransformationEvent( + createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); it( 'should not strip attributes of wrapped list', () => { @@ -248,17 +269,20 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ` ) }] ` ) ); - const modelFragment = model.getSelectedContent( model.createSelection( model.document.getRoot(), 'in' ) ); + view.document.on( 'outputTransformation', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( Array.from( data.content.getChildren() ).every( isListItemBlock ) ).to.be.false; + expect( Array.from( data.content.getChild( 0 ).getChildren() ).every( isListItemBlock ) ).to.be.true; - expect( modelFragment.childCount ).to.equal( 1 ); - expect( Array.from( modelFragment.getChildren() ).every( isListItemBlock ) ).to.be.false; - expect( Array.from( modelFragment.getChild( 0 ).getChildren() ).every( isListItemBlock ) ).to.be.true; - - expect( stringifyModel( modelFragment ) ).to.equal( - '
' + - 'foo' + - '
' - ); + expect( stringifyModel( data.content ) ).to.equal( + '
' + + 'foo' + + '
' + ); + } ); + + clipboard._fireOutputTransformationEvent( + createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); } ); @@ -419,8 +443,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -440,8 +462,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • W
    • X

Y

  • Z
' ) } ); @@ -463,8 +483,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '

X

  • Y
' ) } ); @@ -487,8 +505,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -509,8 +525,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'B' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -528,8 +542,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'B' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -563,8 +575,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'Bar' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
  • ' ) } ); @@ -591,8 +601,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'Bar' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
  • ' ) } ); @@ -615,8 +623,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
    • W
      • X

        Y

        Z
    ' ) } ); @@ -638,8 +644,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
    • W
      • X

        Y

        Z
    ' ) } ); @@ -661,8 +665,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
    • W

      X

      Y

    • Z
    ' ) } ); @@ -695,8 +697,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { conversionApi.updateConversionResult( splitBlock, data ); } ) ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
    • ab
    ' ) } ); @@ -710,4 +710,110 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ); } ); } ); + + describe( 'drag integration', () => { + it( 'should return a list item, when a whole list item was selected and dragged', () => { + setModelData( model, + '[Foo bar.]' + ); + + const elements = Array.from( model.document.selection.getSelectedBlocks() ); + const firstElement = elements[ 0 ]; + const lastElement = elements[ elements.length - 1 ]; + const startPosition = model.createPositionBefore( firstElement ); + const endPosition = model.createPositionAfter( lastElement ); + const blockRange = model.createRange( startPosition, endPosition ); + const draggedRange = LiveRange.fromRange( blockRange ); + + const dataTransferMock = createDataTransfer(); + const draggedSelection = model.createSelection( draggedRange.toRange() ); + + view.document.on( 'dragstart', evt => { + evt.stop(); + } ); + + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( stringifyView( data.content ) ).is.equal( + '
    • Foo bar.

    ' + ); + } ); + + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + } ); + + it( 'should return a list item, when a whole list item was selected and' + + 'end of selection is positioned in first place in next paragraph (triple click)', () => { + setModelData( model, + '[Foo bar.' + + ']' + ); + + const elements = Array.from( model.document.selection.getSelectedBlocks() ); + const firstElement = elements[ 0 ]; + const lastElement = elements[ elements.length - 1 ]; + const startPosition = model.createPositionBefore( firstElement ); + const endPosition = model.createPositionAfter( lastElement ); + const blockRange = model.createRange( startPosition, endPosition ); + const draggedRange = LiveRange.fromRange( blockRange ); + + const dataTransferMock = createDataTransfer(); + const draggedSelection = model.createSelection( draggedRange.toRange() ); + + view.document.on( 'dragstart', evt => { + evt.stop(); + } ); + + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( stringifyView( data.content ) ).is.equal( + '
    • Foo bar.

    ' + ); + } ); + + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + } ); + + it( 'should return all selected content, even when end of selection is positioned in first place in next paragraph', () => { + setModelData( model, + '[Foo bar.' + + ']' + ); + + const elements = Array.from( model.document.selection.getSelectedBlocks() ); + const firstElement = elements[ 0 ]; + const lastElement = elements[ elements.length - 1 ]; + const startPosition = model.createPositionBefore( firstElement ); + const endPosition = model.createPositionAfter( lastElement ); + const blockRange = model.createRange( startPosition, endPosition ); + const draggedRange = LiveRange.fromRange( blockRange ); + + const dataTransferMock = createDataTransfer(); + const draggedSelection = model.createSelection( draggedRange.toRange() ); + + view.document.on( 'dragstart', evt => { + evt.stop(); + } ); + + view.document.on( 'clipboardOutput', ( evt, data ) => { + expect( stringifyView( data.content ) ).is.equal( + '

    Foo bar.

    ' + ); + } ); + + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + } ); + } ); + + function createDataTransfer() { + const store = new Map(); + + return { + setData( type, data ) { + store.set( type, data ); + }, + + getData( type ) { + return store.get( type ); + } + }; + } } );