Skip to content

Commit

Permalink
Merge pull request #15030 from ckeditor/ck/14969-handle-of-list-items…
Browse files Browse the repository at this point in the history
…-selection-and-dropping-document-lists

Fix (list): Dragged selected whole list item should be still a list item after drop. Closes #14969 .
  • Loading branch information
arkflpc committed Sep 26, 2023
2 parents a9eff48 + e911758 commit a20bcfb
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 84 deletions.
28 changes: 20 additions & 8 deletions packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`}
Expand All @@ -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.

Expand Down
86 changes: 77 additions & 9 deletions packages/ckeditor5-clipboard/src/clipboardpipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import type {
DomEventData,
Range,
ViewDocumentFragment,
ViewRange
ViewRange,
Selection,
DocumentSelection
} from '@ckeditor/ckeditor5-engine';

import ClipboardObserver, {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<ClipboardOutputTransformationEvent>( 'outputTransformation', {
dataTransfer,
content,
method
} );
}

/**
* The clipboard paste pipeline.
*/
Expand Down Expand Up @@ -257,13 +283,7 @@ export default class ClipboardPipeline extends Plugin {

data.preventDefault();

const content = editor.data.toView( editor.model.getSelectedContent( modelDocument.selection ) );

viewDocument.fire<ViewDocumentClipboardOutputEvent>( 'clipboardOutput', {
dataTransfer,
content,
method: evt.name
} );
this._fireOutputTransformationEvent( dataTransfer, modelDocument.selection, evt.name );
};

this.listenTo<ViewDocumentCopyEvent>( viewDocument, 'copy', onCopyCut, { priority: 'low' } );
Expand All @@ -277,6 +297,16 @@ export default class ClipboardPipeline extends Plugin {
}
}, { priority: 'low' } );

this.listenTo<ClipboardOutputTransformationEvent>( this, 'outputTransformation', ( evt, data ) => {
const content = editor.data.toView( data.content );

viewDocument.fire<ViewDocumentClipboardOutputEvent>( 'clipboardOutput', {
dataTransfer: data.dataTransfer,
content,
method: data.method
} );
}, { priority: 'low' } );

this.listenTo<ViewDocumentClipboardOutputEvent>( viewDocument, 'clipboardOutput', ( evt, data ) => {
if ( !data.content.isEmpty ) {
data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) );
Expand Down Expand Up @@ -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';
}
11 changes: 3 additions & 8 deletions packages/ckeditor5-clipboard/src/dragdrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ import {
} from '@ckeditor/ckeditor5-utils';

import ClipboardPipeline, {
type ClipboardContentInsertionEvent,
type ViewDocumentClipboardOutputEvent
type ClipboardContentInsertionEvent
} from './clipboardpipeline';

import ClipboardObserver, {
Expand Down Expand Up @@ -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<ViewDocumentClipboardOutputEvent>( 'clipboardOutput', {
dataTransfer: data.dataTransfer,
content,
method: 'dragstart'
} );
clipboardPipeline._fireOutputTransformationEvent( data.dataTransfer, draggedSelection, 'dragstart' );

const { dataTransfer, domTarget, domEvent } = data;
const { clientX } = domEvent;
Expand Down
2 changes: 2 additions & 0 deletions packages/ckeditor5-clipboard/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
type ClipboardContentInsertionEvent,
type ClipboardInputTransformationEvent,
type ClipboardInputTransformationData,
type ClipboardOutputTransformationEvent,
type ClipboardOutputTransformationData,
type ViewDocumentClipboardOutputEvent
} from './clipboardpipeline';

Expand Down
23 changes: 23 additions & 0 deletions packages/ckeditor5-clipboard/tests/clipboardpipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<paragraph>a[bc</paragraph><paragraph>de]f</paragraph>' );

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( '<paragraph>bc</paragraph><paragraph>de</paragraph>' );

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();
Expand Down
23 changes: 23 additions & 0 deletions packages/ckeditor5-clipboard/tests/manual/dragdroplists.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<h2>Classic Editor - document lists</h2>

<div id="editor-classic-lists">
<p></p>
<p></p>
<ul>
<li><strong>Creating new types of editors.</strong> You can create new editor types using the framework.</li>
<li><strong>Writing your own features.</strong> New features are implemented using the framework.</li>
</ul>
<p></p>
<ul>
<li><strong>Customizing existing features.</strong> Changing the behavior or look of existing features can be
done
thanks to the framework’s capabilities.</li>
</ul>
<p></p>
<blockquote>
<ul>
<li><a href="#">Quoted</a> UL List item 1</li>
</ul>
</blockquote>
<p></p>
</div>
107 changes: 107 additions & 0 deletions packages/ckeditor5-clipboard/tests/manual/dragdroplists.js
Original file line number Diff line number Diff line change
@@ -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 );
} );
Empty file.
Loading

0 comments on commit a20bcfb

Please sign in to comment.