From e451d6eebc22a12ce239730a69bdbb8acd471a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Wed, 20 Sep 2023 10:29:33 +0200 Subject: [PATCH 01/14] Change listener from 'getSelectedContent' into 'clipboardOutput'. --- .../tests/manual/dragdropexperimental.html | 23 ++++++++ .../tests/manual/dragdropexperimental.js | 53 +++++++++++++++++++ .../src/documentlist/documentlistediting.ts | 20 +++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html index 43f7d1467a5..ac2bc44a72a 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html +++ b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html @@ -129,6 +129,29 @@

<details> and <summary> tags as HTML s +

Classic Editor - document lists

+ +
+

 

+

 

+ +

+ +

 

+
+ +
+

 

+
+

Decoupled Editor

The toolbar

diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js index 775b13752b4..d4ce365716a 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js @@ -341,3 +341,56 @@ BalloonEditor .catch( err => { console.error( err.stack ); } ); + +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, DragDropExperimental + ], + 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.editorClassic = editor; + + CKEditorInspector.attach( { classicLists: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 3046084f42f..5b7976e7e17 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -67,6 +67,10 @@ import ListWalker, { ListBlocksIterable } from './utils/listwalker'; +import type { + ViewDocumentClipboardOutputEvent +} from 'ckeditor5/src/clipboard'; + import '../../theme/documentlist.css'; import '../../theme/list.css'; @@ -476,6 +480,7 @@ export default class DocumentListEditing extends Plugin { */ private _setupClipboardIntegration() { const model = this.editor.model; + const viewDoc = this.editor.editing.view.document; this.listenTo( model, 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); @@ -506,12 +511,17 @@ 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( viewDoc, 'clipboardOutput', ( evt, data ) => { + if ( data.method != 'dragstart' ) { + const selection = model.document.selection; + const isSingleListItemSelected = isSingleListItem( Array.from( selection.getSelectedBlocks() ) ); - if ( isSingleListItemSelected ) { - model.change( writer => removeListAttributes( Array.from( evt.return!.getChildren() as any ), writer ) ); + if ( isSingleListItemSelected ) { + const content = this.editor.data.toModel( data.content ); + + model.change( writer => removeListAttributes( Array.from( content.getChildren() as any ), writer ) ); + } } } ); } From f656ec1a3dc84f9c224e8872b3b0acd60897642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 21 Sep 2023 09:32:31 +0200 Subject: [PATCH 02/14] Extending Clipboard pipeline by outputTransformation step. --- .../src/clipboardpipeline.ts | 19 +++++++- .../src/dragdropexperimental.ts | 4 +- .../src/documentlist/documentlistediting.ts | 44 +++++++++++-------- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 3a79d725459..29cca37ee90 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -65,6 +65,11 @@ import viewToPlainText from './utils/viewtoplaintext'; // │ │ // └────────────────┌────────────────┘ // │ +// ┌───────────V───────────┐ +// │ ClipboardPipeline │ +// │ outputTransformation │ +// └───────────┬───────────┘ +// TODO │ // ┌─────────V────────┐ // │ view.Document │ Processes view.DocumentFragment to text/html and text/plain // │ clipboardOutput │ and stores the results in data.dataTransfer. @@ -257,9 +262,9 @@ export default class ClipboardPipeline extends Plugin { data.preventDefault(); - const content = editor.data.toView( editor.model.getSelectedContent( modelDocument.selection ) ); + const content = editor.model.getSelectedContent( modelDocument.selection ); - viewDocument.fire( 'clipboardOutput', { + modelDocument.fire( 'outputTransformation', { dataTransfer, content, method: evt.name @@ -277,6 +282,16 @@ export default class ClipboardPipeline extends Plugin { } }, { priority: 'low' } ); + this.listenTo( modelDocument, 'outputTransformation', ( evt, data ) => { + const content = editor.data.toView( data.content ); + + viewDocument.fire( 'clipboardOutput', { + dataTransfer: data.dataTransfer, + content, + method: evt.name + } ); + }, { priority: 'low' } ); + this.listenTo( viewDocument, 'clipboardOutput', ( evt, data ) => { if ( !data.content.isEmpty ) { data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) ); diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index f565bd7654e..6caf66e52ed 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -289,9 +289,9 @@ export default class DragDropExperimental 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 content = model.getSelectedContent( draggedSelection ); - viewDocument.fire( 'clipboardOutput', { + model.document.fire( 'outputTransformation', { dataTransfer: data.dataTransfer, content, method: 'dragstart' diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 5b7976e7e17..ee7fc73024e 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -12,18 +12,19 @@ import { type MultiCommand } from 'ckeditor5/src/core'; -import type { - DowncastAttributeEvent, - DocumentChangeEvent, - DowncastWriter, - Element, - Model, - ModelGetSelectedContentEvent, - ModelInsertContentEvent, - UpcastElementEvent, - ViewDocumentTabEvent, - ViewElement, - Writer +import { + type DowncastAttributeEvent, + type DocumentChangeEvent, + type DowncastWriter, + type Element, + type Model, + type ModelGetSelectedContentEvent, + type ModelInsertContentEvent, + type UpcastElementEvent, + type ViewDocumentTabEvent, + type ViewElement, + type Writer, + UpcastWriter } from 'ckeditor5/src/engine'; import { Delete, type ViewDocumentDeleteEvent } from 'ckeditor5/src/typing'; @@ -512,15 +513,22 @@ export default class DocumentListEditing extends Plugin { // └─────────────────────┴───────────────────┘ // // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969 - this.listenTo( viewDoc, 'clipboardOutput', ( evt, data ) => { + this.listenTo( model.document, 'outputTransformation', ( evt, data ) => { + const writer = new UpcastWriter( data.content.document ); + const allContentChildren = Array.from( data.content.getChildren() ); + const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; + + if ( allContentChildren.length > 1 && lastItem.isEmpty ) { + writer.removeChildren( allContentChildren.length - 1, 1, data.content ); + } + if ( data.method != 'dragstart' ) { - const selection = model.document.selection; - const isSingleListItemSelected = isSingleListItem( Array.from( selection.getSelectedBlocks() ) ); + const isSingleListItemSelected = isSingleListItem( Array.from( data.content.getChildren() ) as any ); if ( isSingleListItemSelected ) { - const content = this.editor.data.toModel( data.content ); - - model.change( writer => removeListAttributes( Array.from( content.getChildren() as any ), writer ) ); + const childrenWithoutListAttributes = model.change( + writer => removeListAttributes( Array.from( data.content.getChildren() as any ), writer ) ); + // TODO? } } } ); From f13b8bee0dc5f8209997d017f4c5e1bfd39704f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 21 Sep 2023 09:45:29 +0200 Subject: [PATCH 03/14] Add manual test only for dragging lists. --- .../tests/manual/dragdropexperimental.html | 23 ---- .../tests/manual/dragdropexperimental.js | 53 --------- .../tests/manual/dragdroplists.html | 23 ++++ .../tests/manual/dragdroplists.js | 111 ++++++++++++++++++ .../tests/manual/dragdroplists.md | 0 5 files changed, 134 insertions(+), 76 deletions(-) create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdroplists.html create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdroplists.js create mode 100644 packages/ckeditor5-clipboard/tests/manual/dragdroplists.md diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html index ac2bc44a72a..43f7d1467a5 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html +++ b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.html @@ -129,29 +129,6 @@

<details> and <summary> tags as HTML s -

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.
  • -
-

 

-
- -
-

 

-
-

Decoupled Editor

The toolbar

diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js index d4ce365716a..775b13752b4 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdropexperimental.js @@ -341,56 +341,3 @@ BalloonEditor .catch( err => { console.error( err.stack ); } ); - -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, DragDropExperimental - ], - 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.editorClassic = editor; - - CKEditorInspector.attach( { classicLists: editor } ); - } ) - .catch( err => { - console.error( err.stack ); - } ); 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..5aa5f50b091 --- /dev/null +++ b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js @@ -0,0 +1,111 @@ +/** + * @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, CKEditorInspector */ + +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 { DragDropExperimental } from '../../src'; + +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, DragDropExperimental + ], + 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.editorClassicLists = editor; + + CKEditorInspector.attach( { classicLists: 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 From ff21eb091b405d810ac0fc7d0ca57cd764621f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 21 Sep 2023 13:58:25 +0200 Subject: [PATCH 04/14] Update the clipboard integration with document lists. --- .../src/clipboardpipeline.ts | 4 +-- .../src/dragdropexperimental.ts | 3 +- .../src/documentlist/documentlistediting.ts | 31 ++++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 29cca37ee90..82554dab551 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -264,7 +264,7 @@ export default class ClipboardPipeline extends Plugin { const content = editor.model.getSelectedContent( modelDocument.selection ); - modelDocument.fire( 'outputTransformation', { + this.fire( 'outputTransformation', { dataTransfer, content, method: evt.name @@ -282,7 +282,7 @@ export default class ClipboardPipeline extends Plugin { } }, { priority: 'low' } ); - this.listenTo( modelDocument, 'outputTransformation', ( evt, data ) => { + this.listenTo( this, 'outputTransformation', ( evt, data ) => { const content = editor.data.toView( data.content ); viewDocument.fire( 'clipboardOutput', { diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index 6caf66e52ed..ea10c42ecba 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -290,8 +290,9 @@ export default class DragDropExperimental extends Plugin { const draggedSelection = model.createSelection( this._draggedRange.toRange() ); const content = model.getSelectedContent( draggedSelection ); + const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - model.document.fire( 'outputTransformation', { + clipboardPipeline.fire( 'outputTransformation', { dataTransfer: data.dataTransfer, content, method: 'dragstart' diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index ee7fc73024e..5d8082da5d9 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -69,6 +69,7 @@ import ListWalker, { } from './utils/listwalker'; import type { + ClipboardPipeline, ViewDocumentClipboardOutputEvent } from 'ckeditor5/src/clipboard'; @@ -481,7 +482,7 @@ export default class DocumentListEditing extends Plugin { */ private _setupClipboardIntegration() { const model = this.editor.model; - const viewDoc = this.editor.editing.view.document; + const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); this.listenTo( model, 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } ); @@ -513,24 +514,24 @@ export default class DocumentListEditing extends Plugin { // └─────────────────────┴───────────────────┘ // // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969 - this.listenTo( model.document, 'outputTransformation', ( evt, data ) => { - const writer = new UpcastWriter( data.content.document ); - const allContentChildren = Array.from( data.content.getChildren() ); - const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; + this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { + model.change( writer => { + const allContentChildren = Array.from( data.content.getChildren() ); + const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; - if ( allContentChildren.length > 1 && lastItem.isEmpty ) { - writer.removeChildren( allContentChildren.length - 1, 1, data.content ); - } + if ( allContentChildren.length > 1 && lastItem.isEmpty ) { + writer.remove( lastItem ); + } - if ( data.method != 'dragstart' ) { - const isSingleListItemSelected = isSingleListItem( Array.from( data.content.getChildren() ) as any ); + if ( data.method != 'dragstart' ) { + const allChildren = Array.from( data.content.getChildren() ) as any; + const isSingleListItemSelected = isSingleListItem( allChildren ); - if ( isSingleListItemSelected ) { - const childrenWithoutListAttributes = model.change( - writer => removeListAttributes( Array.from( data.content.getChildren() as any ), writer ) ); - // TODO? + if ( isSingleListItemSelected ) { + removeListAttributes( allChildren, writer ); + } } - } + } ); } ); } } From 8834999cc95bcaaec1f76a6c92de5c798ca00d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 21 Sep 2023 22:39:47 +0200 Subject: [PATCH 05/14] Add type on OutputTransformation event. --- .../src/clipboardpipeline.ts | 38 +++++++++++++++++-- .../src/dragdropexperimental.ts | 6 +-- packages/ckeditor5-clipboard/src/index.ts | 3 +- .../src/documentlist/documentlistediting.ts | 4 +- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 82554dab551..d5c0bfe47e1 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -264,7 +264,7 @@ export default class ClipboardPipeline extends Plugin { const content = editor.model.getSelectedContent( modelDocument.selection ); - this.fire( 'outputTransformation', { + this.fire( 'outputTransformation', { dataTransfer, content, method: evt.name @@ -282,13 +282,13 @@ export default class ClipboardPipeline extends Plugin { } }, { priority: 'low' } ); - this.listenTo( this, 'outputTransformation', ( evt, data ) => { + this.listenTo( this, 'outputTransformation', ( evt, data ) => { const content = editor.data.toView( data.content ); viewDocument.fire( 'clipboardOutput', { dataTransfer: data.dataTransfer, content, - method: evt.name + method: data.method } ); }, { priority: 'low' } ); @@ -456,3 +456,35 @@ export interface ViewDocumentClipboardOutputEventData { */ method: 'copy' | 'cut' | 'dragstart'; } + +/** + * TODO + */ +export type ModelDocumentOutputTransformationEvent = { + name: 'outputTransformation'; + args: [ data: ModelDocumentOutputTransformationEventData ]; +}; + +/** + * The value of the 'outputTransformation' event. + */ +export interface ModelDocumentOutputTransformationEventData { + + /** + * 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/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index ea10c42ecba..fe8489af7dc 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -43,8 +43,8 @@ import { } from '@ckeditor/ckeditor5-utils'; import ClipboardPipeline, { - type ClipboardContentInsertionEvent, - type ViewDocumentClipboardOutputEvent + type ModelDocumentOutputTransformationEvent, + type ClipboardContentInsertionEvent } from './clipboardpipeline'; import ClipboardObserver, { @@ -292,7 +292,7 @@ export default class DragDropExperimental extends Plugin { const content = model.getSelectedContent( draggedSelection ); const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - clipboardPipeline.fire( 'outputTransformation', { + clipboardPipeline.fire( 'outputTransformation', { dataTransfer: data.dataTransfer, content, method: 'dragstart' diff --git a/packages/ckeditor5-clipboard/src/index.ts b/packages/ckeditor5-clipboard/src/index.ts index 23439f565b5..28178218ca6 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -13,7 +13,8 @@ export { type ClipboardContentInsertionEvent, type ClipboardInputTransformationEvent, type ClipboardInputTransformationData, - type ViewDocumentClipboardOutputEvent + type ViewDocumentClipboardOutputEvent, + type ModelDocumentOutputTransformationEvent } from './clipboardpipeline'; export type { diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 5d8082da5d9..63b1d0b2521 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -70,7 +70,7 @@ import ListWalker, { import type { ClipboardPipeline, - ViewDocumentClipboardOutputEvent + ModelDocumentOutputTransformationEvent } from 'ckeditor5/src/clipboard'; import '../../theme/documentlist.css'; @@ -514,7 +514,7 @@ export default class DocumentListEditing extends Plugin { // └─────────────────────┴───────────────────┘ // // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969 - this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { + this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { model.change( writer => { const allContentChildren = Array.from( data.content.getChildren() ); const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; From 234b510887bf506d97bd3df9979683f11a89835a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Sat, 23 Sep 2023 14:25:21 +0200 Subject: [PATCH 06/14] Rename the Clipboard event type to match the convention. --- .../src/clipboardpipeline.ts | 10 +++--- .../src/dragdropexperimental.ts | 4 +-- packages/ckeditor5-clipboard/src/index.ts | 2 +- .../src/documentlist/documentlistediting.ts | 32 +++++++++---------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index d5c0bfe47e1..c413991a285 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -264,7 +264,7 @@ export default class ClipboardPipeline extends Plugin { const content = editor.model.getSelectedContent( modelDocument.selection ); - this.fire( 'outputTransformation', { + this.fire( 'outputTransformation', { dataTransfer, content, method: evt.name @@ -282,7 +282,7 @@ export default class ClipboardPipeline extends Plugin { } }, { priority: 'low' } ); - this.listenTo( this, 'outputTransformation', ( evt, data ) => { + this.listenTo( this, 'outputTransformation', ( evt, data ) => { const content = editor.data.toView( data.content ); viewDocument.fire( 'clipboardOutput', { @@ -460,15 +460,15 @@ export interface ViewDocumentClipboardOutputEventData { /** * TODO */ -export type ModelDocumentOutputTransformationEvent = { +export type ClipboardOutputTransformationEvent = { name: 'outputTransformation'; - args: [ data: ModelDocumentOutputTransformationEventData ]; + args: [ data: ClipboardOutputTransformationEventData ]; }; /** * The value of the 'outputTransformation' event. */ -export interface ModelDocumentOutputTransformationEventData { +export interface ClipboardOutputTransformationEventData { /** * The data transfer instance. diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index fe8489af7dc..6d01f78cf34 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -43,7 +43,7 @@ import { } from '@ckeditor/ckeditor5-utils'; import ClipboardPipeline, { - type ModelDocumentOutputTransformationEvent, + type ClipboardOutputTransformationEvent, type ClipboardContentInsertionEvent } from './clipboardpipeline'; @@ -292,7 +292,7 @@ export default class DragDropExperimental extends Plugin { const content = model.getSelectedContent( draggedSelection ); const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - clipboardPipeline.fire( 'outputTransformation', { + clipboardPipeline.fire( 'outputTransformation', { dataTransfer: data.dataTransfer, content, method: 'dragstart' diff --git a/packages/ckeditor5-clipboard/src/index.ts b/packages/ckeditor5-clipboard/src/index.ts index 28178218ca6..6a0e18120d6 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -14,7 +14,7 @@ export { type ClipboardInputTransformationEvent, type ClipboardInputTransformationData, type ViewDocumentClipboardOutputEvent, - type ModelDocumentOutputTransformationEvent + type ClipboardOutputTransformationEvent } from './clipboardpipeline'; export type { diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 63b1d0b2521..44c26b5acfa 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -12,19 +12,17 @@ import { type MultiCommand } from 'ckeditor5/src/core'; -import { - type DowncastAttributeEvent, - type DocumentChangeEvent, - type DowncastWriter, - type Element, - type Model, - type ModelGetSelectedContentEvent, - type ModelInsertContentEvent, - type UpcastElementEvent, - type ViewDocumentTabEvent, - type ViewElement, - type Writer, - UpcastWriter +import type { + DowncastAttributeEvent, + DocumentChangeEvent, + DowncastWriter, + Element, + Model, + ModelInsertContentEvent, + UpcastElementEvent, + ViewDocumentTabEvent, + ViewElement, + Writer } from 'ckeditor5/src/engine'; import { Delete, type ViewDocumentDeleteEvent } from 'ckeditor5/src/typing'; @@ -68,9 +66,9 @@ import ListWalker, { ListBlocksIterable } from './utils/listwalker'; -import type { +import { ClipboardPipeline, - ModelDocumentOutputTransformationEvent + type ClipboardOutputTransformationEvent } from 'ckeditor5/src/clipboard'; import '../../theme/documentlist.css'; @@ -110,7 +108,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; } /** @@ -514,7 +512,7 @@ export default class DocumentListEditing extends Plugin { // └─────────────────────┴───────────────────┘ // // See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969 - this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { + this.listenTo( clipboardPipeline, 'outputTransformation', ( evt, data ) => { model.change( writer => { const allContentChildren = Array.from( data.content.getChildren() ); const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; From ee26ec58e39eb09060f20491dbb333f6ec58183f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Mon, 25 Sep 2023 09:09:30 +0200 Subject: [PATCH 07/14] Extract invoking ClipboardOutput event into separate method. --- .../src/clipboardpipeline.ts | 11 ++- .../src/dragdropexperimental.ts | 2 +- .../documentlist/integrations/clipboard.js | 84 +++++++++++++------ 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index c413991a285..9863b7c3c7e 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -69,7 +69,7 @@ import viewToPlainText from './utils/viewtoplaintext'; // │ ClipboardPipeline │ // │ outputTransformation │ // └───────────┬───────────┘ -// TODO │ +// │ // ┌─────────V────────┐ // │ view.Document │ Processes view.DocumentFragment to text/html and text/plain // │ clipboardOutput │ and stores the results in data.dataTransfer. @@ -145,6 +145,13 @@ export default class ClipboardPipeline extends Plugin { return 'ClipboardPipeline' as const; } + /** + * TODO + */ + public fireOutputTransformationEvent( data: ClipboardOutputTransformationEventData ): void { + this.fire( 'outputTransformation', data ); + } + /** * @inheritDoc */ @@ -264,7 +271,7 @@ export default class ClipboardPipeline extends Plugin { const content = editor.model.getSelectedContent( modelDocument.selection ); - this.fire( 'outputTransformation', { + this.fireOutputTransformationEvent( { dataTransfer, content, method: evt.name diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index 6d01f78cf34..69a14cb3561 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -292,7 +292,7 @@ export default class DragDropExperimental extends Plugin { const content = model.getSelectedContent( draggedSelection ); const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - clipboardPipeline.fire( 'outputTransformation', { + clipboardPipeline.fireOutputTransformationEvent( { dataTransfer: data.dataTransfer, content, method: 'dragstart' diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js index 35d6f5f8340..1ae9b9d312e 100644 --- a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js +++ b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js @@ -33,7 +33,7 @@ import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/vie 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 +58,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(); @@ -127,6 +129,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ] ) ); const modelFragment = model.getSelectedContent( model.document.selection ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 1 ); expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; @@ -140,6 +149,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ] ) ); const modelFragment = model.getSelectedContent( model.document.selection ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 1 ); expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; @@ -151,6 +167,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ] ) ); const modelFragment = model.getSelectedContent( model.document.selection ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 1 ); expect( hasAnyListAttribute( modelFragment.getChild( 0 ) ) ).to.be.false; @@ -164,6 +187,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ] ) ); const modelFragment = model.getSelectedContent( model.document.selection ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 3 ); expect( Array.from( modelFragment.getChildren() ).some( isListItemBlock ) ).to.be.false; @@ -236,6 +266,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { // // 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' ) ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 1 ); expect( Array.from( modelFragment.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; @@ -249,6 +286,13 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ` ) ); const modelFragment = model.getSelectedContent( model.createSelection( model.document.getRoot(), 'in' ) ); + const dataTransferMock = createDataTransfer(); + + clipboard.fireOutputTransformationEvent( { + dataTransfer: dataTransferMock, + content: modelFragment, + method: 'copy' + } ); expect( modelFragment.childCount ).to.equal( 1 ); expect( Array.from( modelFragment.getChildren() ).every( isListItemBlock ) ).to.be.false; @@ -260,6 +304,20 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { '' ); } ); + + function createDataTransfer() { + const store = new Map(); + + return { + setData( type, data ) { + store.set( type, data ); + }, + + getData( type ) { + return store.get( type ); + } + }; + } } ); describe( 'preserving list structure when a cross-list item selection existed', () => { @@ -419,8 +477,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -440,8 +496,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 +517,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '

X

  • Y
' ) } ); @@ -487,8 +539,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'C' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -509,8 +559,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'B' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -528,8 +576,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'B' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
' ) } ); @@ -563,8 +609,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'Bar' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
  • ' ) } ); @@ -591,8 +635,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 'Bar' ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
  • X
    • Y
  • ' ) } ); @@ -615,8 +657,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 +678,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 +699,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 +731,6 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { conversionApi.updateConversionResult( splitBlock, data ); } ) ); - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - clipboard.fire( 'inputTransformation', { content: parseView( '
    • ab
    ' ) } ); From e7ac6c31167e45fafe1b3fd4e104ee5b5f551c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Mon, 25 Sep 2023 17:12:35 +0200 Subject: [PATCH 08/14] Tests + code refactor. --- .../src/clipboardpipeline.ts | 24 ++- .../src/dragdropexperimental.ts | 7 +- .../documentlist/integrations/clipboard.js | 190 +++++++++++------- 3 files changed, 132 insertions(+), 89 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 9863b7c3c7e..e7288bd6e1f 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, { @@ -148,7 +150,17 @@ export default class ClipboardPipeline extends Plugin { /** * TODO */ - public fireOutputTransformationEvent( data: ClipboardOutputTransformationEventData ): void { + public fireOutputTransformationEvent( + dataTransfer: DataTransfer, + selection: Selection | DocumentSelection, + method: 'copy' | 'cut' | 'dragstart' + ): void { + const content = this.editor.model.getSelectedContent( selection ); + const data = { + dataTransfer, + content, + method + }; this.fire( 'outputTransformation', data ); } @@ -269,13 +281,7 @@ export default class ClipboardPipeline extends Plugin { data.preventDefault(); - const content = editor.model.getSelectedContent( modelDocument.selection ); - - this.fireOutputTransformationEvent( { - dataTransfer, - content, - method: evt.name - } ); + this.fireOutputTransformationEvent( dataTransfer, modelDocument.selection, evt.name ); }; this.listenTo( viewDocument, 'copy', onCopyCut, { priority: 'low' } ); diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index 69a14cb3561..5ced31a8c9c 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -289,14 +289,9 @@ export default class DragDropExperimental extends Plugin { data.dataTransfer.setData( 'application/ckeditor5-dragging-uid', this._draggingUid ); const draggedSelection = model.createSelection( this._draggedRange.toRange() ); - const content = model.getSelectedContent( draggedSelection ); const clipboardPipeline: ClipboardPipeline = this.editor.plugins.get( 'ClipboardPipeline' ); - clipboardPipeline.fireOutputTransformationEvent( { - 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-list/tests/documentlist/integrations/clipboard.js b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js index 1ae9b9d312e..1062fd0f567 100644 --- a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js +++ b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js @@ -28,7 +28,14 @@ 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'; @@ -128,17 +135,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { '* []' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); - const dataTransferMock = createDataTransfer(); - - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + 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', () => { @@ -148,17 +150,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ' bar' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); - const dataTransferMock = createDataTransfer(); - - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + 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', () => { @@ -166,17 +163,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { '* []' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); - const dataTransferMock = createDataTransfer(); - - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + 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', () => { @@ -186,17 +178,12 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ' B]az' ] ) ); - const modelFragment = model.getSelectedContent( model.document.selection ); - const dataTransferMock = createDataTransfer(); - - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + 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', () => { @@ -265,17 +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' ) ); - const dataTransferMock = createDataTransfer(); - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + view.document.on( 'outputTransformation', ( evt, data ) => { + expect( data.content.childCount ).to.equal( 1 ); + expect( Array.from( data.content.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; } ); - expect( modelFragment.childCount ).to.equal( 1 ); - expect( Array.from( modelFragment.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; + clipboard.fireOutputTransformationEvent( + createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); it( 'should not strip attributes of wrapped list', () => { @@ -285,39 +269,21 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ` ) }] ` ) ); - const modelFragment = model.getSelectedContent( model.createSelection( model.document.getRoot(), 'in' ) ); - const dataTransferMock = createDataTransfer(); + 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; - clipboard.fireOutputTransformationEvent( { - dataTransfer: dataTransferMock, - content: modelFragment, - method: 'copy' + expect( stringifyModel( data.content ) ).to.equal( + '
    ' + + 'foo' + + '
    ' + ); } ); - 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' + - '
    ' - ); + clipboard.fireOutputTransformationEvent( + createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); - - function createDataTransfer() { - const store = new Map(); - - return { - setData( type, data ) { - store.set( type, data ); - }, - - getData( type ) { - return store.get( type ); - } - }; - } } ); describe( 'preserving list structure when a cross-list item selection existed', () => { @@ -744,4 +710,80 @@ 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' ); + } ); + } ); + + function createDataTransfer() { + const store = new Map(); + + return { + setData( type, data ) { + store.set( type, data ); + }, + + getData( type ) { + return store.get( type ); + } + }; + } } ); From f51fca2d324d3caada3375e849cc54a091113eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Mon, 25 Sep 2023 21:02:17 +0200 Subject: [PATCH 09/14] Remove last empty element only when dragged content is a document list. --- .../src/documentlist/documentlistediting.ts | 7 ++++- .../documentlist/integrations/clipboard.js | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 44c26b5acfa..b8af0610fa7 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -518,7 +518,12 @@ export default class DocumentListEditing extends Plugin { const lastItem = allContentChildren[ allContentChildren.length - 1 ] as Element; if ( allContentChildren.length > 1 && lastItem.isEmpty ) { - writer.remove( lastItem ); + const contentChildrenExceptLastItem = Array.from( data.content.getChildren() ).slice( 0, -1 ); + const isContentDocumentList = contentChildrenExceptLastItem!.every( isListItemBlock ); + + if ( isContentDocumentList ) { + writer.remove( lastItem ); + } } if ( data.method != 'dragstart' ) { diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js index 1062fd0f567..91fa5aa5664 100644 --- a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js +++ b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js @@ -771,6 +771,36 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { 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() { From f4523b484f0301799abafa0d6ac106accffab68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Tue, 26 Sep 2023 08:11:17 +0200 Subject: [PATCH 10/14] API docs. [skip ci] --- .../ckeditor5-clipboard/src/clipboardpipeline.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index e7288bd6e1f..bcfa4fdd5da 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -148,7 +148,11 @@ export default class ClipboardPipeline extends Plugin { } /** - * TODO + * Fires Clipboard `'outputTransformation'` event for given parameters. + * + * @param dataTransfer + * @param selection + * @param method */ public fireOutputTransformationEvent( dataTransfer: DataTransfer, @@ -471,7 +475,13 @@ export interface ViewDocumentClipboardOutputEventData { } /** - * TODO + * 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 module:engine/view/document~Document#clipboardOutput + * @param data The event data. */ export type ClipboardOutputTransformationEvent = { name: 'outputTransformation'; From a95acb66f6ef411e3b7e49e78d05464f3f4c50b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Tue, 26 Sep 2023 08:51:58 +0200 Subject: [PATCH 11/14] Update clipboard guide. --- .../docs/framework/deep-dive/clipboard.md | 28 +++++++++++++------ .../src/clipboardpipeline.ts | 8 +++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md b/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md index 7352fcbaedb..24bb415d418 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 + │ view.Document │ 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 bcfa4fdd5da..0002b122926 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -62,14 +62,14 @@ 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 │ -// │ outputTransformation │ +// │ ClipboardPipeline │ Processes model.DocumentFragment and converts it to +// │ outputTransformation │ view.DocumentFragment. // └───────────┬───────────┘ // │ // ┌─────────V────────┐ @@ -480,7 +480,7 @@ export interface ViewDocumentClipboardOutputEventData { * * It is a part of the {@glink framework/deep-dive/clipboard#output-pipeline clipboard output pipeline}. * - * @eventName module:engine/view/document~Document#clipboardOutput + * @eventName module:engine/view/document~Document#outputTransformation * @param data The event data. */ export type ClipboardOutputTransformationEvent = { From a39c9bcc3e0352bb1cf5faf3f4bfd7576a4b8ddd Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 26 Sep 2023 14:29:23 +0200 Subject: [PATCH 12/14] Review fixes. --- .../docs/framework/deep-dive/clipboard.md | 2 +- .../src/clipboardpipeline.ts | 46 +++++++++---------- packages/ckeditor5-clipboard/src/dragdrop.ts | 11 ++--- packages/ckeditor5-clipboard/src/index.ts | 5 +- .../tests/clipboardpipeline.js | 23 ++++++++++ .../tests/manual/dragdroplists.js | 10 ++-- 6 files changed, 55 insertions(+), 42 deletions(-) diff --git a/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md b/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md index 24bb415d418..4ddfdef4351 100644 --- a/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md +++ b/packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md @@ -237,7 +237,7 @@ The output pipeline is the equivalent of the input pipeline but for the copy and └────────────────┌────────────────┘ │ ┌─────────────V────────────┐ Processes model.DocumentFragment - │ view.Document │ and converts it to + │ ClipboardPipeline │ and converts it to │ outputTransformation │ view.DocumentFragment. └──────────────────────────┘ │ diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 0002b122926..ee071ee20fa 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -147,38 +147,36 @@ export default class ClipboardPipeline extends Plugin { return 'ClipboardPipeline' as const; } + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const view = editor.editing.view; + + view.addObserver( ClipboardObserver ); + + this._setupPasteDrop(); + this._setupCopyCut(); + } + /** * Fires Clipboard `'outputTransformation'` event for given parameters. * - * @param dataTransfer - * @param selection - * @param method + * @internal */ - public fireOutputTransformationEvent( + public _fireOutputTransformationEvent( dataTransfer: DataTransfer, selection: Selection | DocumentSelection, method: 'copy' | 'cut' | 'dragstart' ): void { const content = this.editor.model.getSelectedContent( selection ); - const data = { + + this.fire( 'outputTransformation', { dataTransfer, content, method - }; - this.fire( 'outputTransformation', data ); - } - - /** - * @inheritDoc - */ - public init(): void { - const editor = this.editor; - const view = editor.editing.view; - - view.addObserver( ClipboardObserver ); - - this._setupPasteDrop(); - this._setupCopyCut(); + } ); } /** @@ -285,7 +283,7 @@ export default class ClipboardPipeline extends Plugin { data.preventDefault(); - this.fireOutputTransformationEvent( dataTransfer, modelDocument.selection, evt.name ); + this._fireOutputTransformationEvent( dataTransfer, modelDocument.selection, evt.name ); }; this.listenTo( viewDocument, 'copy', onCopyCut, { priority: 'low' } ); @@ -480,18 +478,18 @@ export interface ViewDocumentClipboardOutputEventData { * * It is a part of the {@glink framework/deep-dive/clipboard#output-pipeline clipboard output pipeline}. * - * @eventName module:engine/view/document~Document#outputTransformation + * @eventName ~ClipboardPipeline##outputTransformation * @param data The event data. */ export type ClipboardOutputTransformationEvent = { name: 'outputTransformation'; - args: [ data: ClipboardOutputTransformationEventData ]; + args: [ data: ClipboardOutputTransformationData ]; }; /** * The value of the 'outputTransformation' event. */ -export interface ClipboardOutputTransformationEventData { +export interface ClipboardOutputTransformationData { /** * The data transfer instance. 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 ae704191eaa..ac05573bb07 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -13,8 +13,9 @@ export { type ClipboardContentInsertionEvent, type ClipboardInputTransformationEvent, type ClipboardInputTransformationData, - type ViewDocumentClipboardOutputEvent, - type ClipboardOutputTransformationEvent + type ClipboardOutputTransformationEvent, + type ClipboardOutputTransformationData, + type ViewDocumentClipboardOutputEvent } from './clipboardpipeline'; export type { 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.js b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js index 5aa5f50b091..729742a58d9 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdroplists.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console, window, document, CKEditorInspector */ +/* globals console, window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import { Essentials } from '@ckeditor/ckeditor5-essentials'; @@ -53,8 +53,6 @@ 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 { DragDropExperimental } from '../../src'; - import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; ClassicEditor @@ -66,7 +64,7 @@ ClassicEditor CodeBlock, DocumentListProperties, TableProperties, TableCellProperties, TableCaption, TableColumnResize, EasyImage, ImageResize, ImageInsert, LinkImage, AutoImage, HtmlEmbed, AutoLink, Mention, TextTransformation, Alignment, IndentBlock, PageBreak, HorizontalLine, - CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport, DragDropExperimental + CloudServices, TextPartLanguage, SourceEditing, Style, GeneralHtmlSupport ], toolbar: [ 'heading', 'style', @@ -102,9 +100,7 @@ ClassicEditor } } ) .then( editor => { - window.editorClassicLists = editor; - - CKEditorInspector.attach( { classicLists: editor } ); + window.editor = editor; } ) .catch( err => { console.error( err.stack ); From e5ca348dc77b1b8ecef6ddea86338b234ea50fb8 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 26 Sep 2023 14:47:46 +0200 Subject: [PATCH 13/14] Review fixes. --- .../documentlist/integrations/clipboard.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js index 91fa5aa5664..ceb75b2f0b3 100644 --- a/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js +++ b/packages/ckeditor5-list/tests/documentlist/integrations/clipboard.js @@ -140,7 +140,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; } ); - clipboard.fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); + 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', () => { @@ -155,7 +155,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; } ); - clipboard.fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should strip other list attributes', () => { @@ -168,7 +168,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { expect( hasAnyListAttribute( data.content.getChild( 0 ) ) ).to.be.false; } ); - clipboard.fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); + 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', () => { @@ -183,7 +183,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { expect( Array.from( data.content.getChildren() ).some( isListItemBlock ) ).to.be.false; } ); - clipboard.fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.document.selection, 'copy' ); } ); it( 'should return just a text, if a list item block was partially selected', () => { @@ -258,7 +258,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { expect( Array.from( data.content.getChildren() ).some( hasAnyListAttribute ) ).to.be.false; } ); - clipboard.fireOutputTransformationEvent( + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); @@ -281,7 +281,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ); } ); - clipboard.fireOutputTransformationEvent( + clipboard._fireOutputTransformationEvent( createDataTransfer(), model.createSelection( model.document.getRoot(), 'in' ), 'copy' ); } ); } ); @@ -738,7 +738,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ); } ); - clipboard.fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); } ); it( 'should return a list item, when a whole list item was selected and' + @@ -769,7 +769,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ); } ); - clipboard.fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); } ); it( 'should return all selected content, even when end of selection is positioned in first place in next paragraph', () => { @@ -799,7 +799,7 @@ describe( 'DocumentListEditing integrations: clipboard copy & paste', () => { ); } ); - clipboard.fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); + clipboard._fireOutputTransformationEvent( dataTransferMock, draggedSelection, 'dragstart' ); } ); } ); From e91175884cde67af76517e7609f5f9343cd6cffa Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 26 Sep 2023 14:48:03 +0200 Subject: [PATCH 14/14] Review fixes. --- .../src/documentlist/documentlistediting.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts index 6bb9e99f433..862e6a055d9 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.ts +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.ts @@ -575,24 +575,25 @@ export default class DocumentListEditing extends Plugin { // 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 ] as Element; + const lastItem = allContentChildren[ allContentChildren.length - 1 ]; - if ( allContentChildren.length > 1 && lastItem.isEmpty ) { - const contentChildrenExceptLastItem = Array.from( data.content.getChildren() ).slice( 0, -1 ); - const isContentDocumentList = contentChildrenExceptLastItem!.every( isListItemBlock ); + if ( allContentChildren.length > 1 && lastItem.is( 'element' ) && lastItem.isEmpty ) { + const contentChildrenExceptLastItem = allContentChildren.slice( 0, -1 ); - if ( isContentDocumentList ) { + if ( contentChildrenExceptLastItem.every( isListItemBlock ) ) { writer.remove( lastItem ); } } - if ( data.method != 'dragstart' ) { - const allChildren = Array.from( data.content.getChildren() ) as any; + // 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, writer ); + removeListAttributes( allChildren as Array, writer ); } } } );