Skip to content

Commit

Permalink
Extend range of drag-and-drop selection to fully selected parents (#1…
Browse files Browse the repository at this point in the history
…4891)

Feature (clipboard): Extend drag-and-drop selection to parent elements when all their children are selected. Closes #14640.
  • Loading branch information
filipsobol authored Sep 15, 2023
1 parent 76663b4 commit 5e26217
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 36 deletions.
103 changes: 72 additions & 31 deletions packages/ckeditor5-clipboard/src/dragdropexperimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
MouseObserver,
type DataTransfer,
type Element,
type Model,
type Range,
type Position,
type ViewDocumentMouseDownEvent,
type ViewDocumentMouseUpEvent,
type ViewElement,
Expand Down Expand Up @@ -580,41 +583,39 @@ export default class DragDropExperimental extends Plugin {

widgetToolbarRepository.forceDisabled( 'dragDrop' );
}

return;
}

// If this was not a widget we should check if we need to drag some text content.
else if ( !selection.isCollapsed || ( selection.getFirstPosition()!.parent as Element ).isEmpty ) {
const blocks = Array.from( selection.getSelectedBlocks() );

if ( blocks.length > 1 ) {
this._draggedRange = LiveRange.fromRange( model.createRange(
model.createPositionBefore( blocks[ 0 ] ),
model.createPositionAfter( blocks[ blocks.length - 1 ] )
) );

model.change( writer => writer.setSelection( this._draggedRange!.toRange() ) );
this._blockMode = true;
// TODO block mode for dragging from outside editor? or inline? or both?
}
else if ( blocks.length == 1 ) {
const draggedRange = selection.getFirstRange()!;
const blockRange = model.createRange(
model.createPositionBefore( blocks[ 0 ] ),
model.createPositionAfter( blocks[ 0 ] )
);

if (
draggedRange.start.isTouching( blockRange.start ) &&
draggedRange.end.isTouching( blockRange.end )
) {
this._draggedRange = LiveRange.fromRange( blockRange );
this._blockMode = true;
} else {
this._draggedRange = LiveRange.fromRange( selection.getFirstRange()! );
this._blockMode = false;
}
}
if ( selection.isCollapsed && !( selection.getFirstPosition()!.parent as Element ).isEmpty ) {
return;
}

const blocks = Array.from( selection.getSelectedBlocks() );
const draggedRange = selection.getFirstRange()!;

if ( blocks.length == 0 ) {
this._draggedRange = LiveRange.fromRange( draggedRange );

return;
}

const blockRange = getRangeIncludingFullySelectedParents( model, blocks );

if ( blocks.length > 1 ) {
this._draggedRange = LiveRange.fromRange( blockRange );
this._blockMode = true;
// TODO block mode for dragging from outside editor? or inline? or both?
} else if ( blocks.length == 1 ) {
const touchesBlockEdges = draggedRange.start.isTouching( blockRange.start ) &&
draggedRange.end.isTouching( blockRange.end );

this._draggedRange = LiveRange.fromRange( touchesBlockEdges ? blockRange : draggedRange );
this._blockMode = touchesBlockEdges;
}

model.change( writer => writer.setSelection( this._draggedRange!.toRange() ) );
}

/**
Expand Down Expand Up @@ -690,3 +691,43 @@ function findDraggableWidget( target: ViewElement ): ViewElement | null {

return null;
}

/**
* Recursively checks if common parent of provided elements doesn't have any other children. If that's the case,
* it returns range including this parent. Otherwise, it returns only the range from first to last element.
*
* Example:
*
* <blockQuote>
* <paragraph>[Test 1</paragraph>
* <paragraph>Test 2</paragraph>
* <paragraph>Test 3]</paragraph>
* <blockQuote>
*
* Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too.
* If only first and second paragraphs would be selected, the range would not include it.
*/
function getRangeIncludingFullySelectedParents( model: Model, elements: Array<Element> ): Range {
const firstElement = elements[ 0 ];
const lastElement = elements[ elements.length - 1 ];
const parent = firstElement.getCommonAncestor( lastElement );
const startPosition: Position = model.createPositionBefore( firstElement );
const endPosition: Position = model.createPositionAfter( lastElement );

if (
parent &&
parent.is( 'element' ) &&
!model.schema.isLimit( parent )
) {
const parentRange = model.createRangeOn( parent );
const touchesStart = startPosition.isTouching( parentRange.start );
const touchesEnd = endPosition.isTouching( parentRange.end );

if ( touchesStart && touchesEnd ) {
// Selection includes all elements in the parent.
return getRangeIncludingFullySelectedParents( model, [ parent ] );
}
}

return model.createRange( startPosition, endPosition );
}
191 changes: 186 additions & 5 deletions packages/ckeditor5-clipboard/tests/dragdropexperimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli
import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import { Image, ImageCaption } from '@ckeditor/ckeditor5-image';
import env from '@ckeditor/ckeditor5-utils/src/env';

import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
Expand Down Expand Up @@ -64,7 +65,18 @@ describe( 'Drag and Drop experimental', () => {
document.body.appendChild( editorElement );

editor = await ClassicTestEditor.create( editorElement, {
plugins: [ DragDropExperimental, PastePlainText, Paragraph, Table, HorizontalLine, ShiftEnter, BlockQuote, Bold ]
plugins: [
DragDropExperimental,
PastePlainText,
Paragraph,
Table,
HorizontalLine,
ShiftEnter,
BlockQuote,
Bold,
Image,
ImageCaption
]
} );

model = editor.model;
Expand Down Expand Up @@ -494,10 +506,8 @@ describe( 'Drag and Drop experimental', () => {
} );

it( 'should not remove dragged range if insert into drop target was not allowed', () => {
editor.model.schema.register( 'caption', {
allowIn: '$root',
allowContentOf: '$block',
isObject: true
editor.model.schema.extend( 'caption', {
allowIn: '$root'
} );

editor.conversion.elementToElement( {
Expand Down Expand Up @@ -1246,6 +1256,44 @@ describe( 'Drag and Drop experimental', () => {
);
} );

it( 'should start dragging text from caption to paragraph', () => {
setModelData( model, trim`
<imageBlock src="">
<caption>[World]</caption>
</imageBlock>
<paragraph>Hello</paragraph>
` );

const dataTransferMock = createDataTransfer();
const viewElement = viewDocument.getRoot().getChild( 1 );
const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' );

viewDocument.fire( 'dragstart', {
domTarget: domConverter.mapViewToDom( viewElement ),
target: viewElement,
domEvent: {},
dataTransfer: dataTransferMock,
stopPropagation: () => {}
} );

expect( dataTransferMock.getData( 'text/html' ) ).to.equal( 'World' );

fireDragging( dataTransferMock, positionAfterHr );
expectDraggingMarker( positionAfterHr );

fireDrop(
dataTransferMock,
model.createPositionAt( root.getChild( 1 ), 5 )
);

expect( getModelData( model ) ).to.equal( trim`
<imageBlock src="">
<caption></caption>
</imageBlock>
<paragraph>HelloWorld[]</paragraph>
` );
} );

it( 'should not drag parent paragraph when only portion of content is selected', () => {
setModelData( model,
'<paragraph>foobar</paragraph>' +
Expand Down Expand Up @@ -1910,6 +1958,132 @@ describe( 'Drag and Drop experimental', () => {
expect( data.targetRanges[ 0 ].isEqual( view.createRangeOn( viewDocument.getRoot().getChild( 1 ) ) ) ).to.be.true;
} );
} );

describe( 'extending selection range when all parent elements are selected', () => {
it( 'extends flat selection', () => {
setModelData( model, trim`
<blockQuote>
<paragraph>[one</paragraph>
<paragraph>two</paragraph>
<paragraph>three]</paragraph>
</blockQuote>
<horizontalLine></horizontalLine>
` );

const dataTransferMock = createDataTransfer();
const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' );

fireDragStart( dataTransferMock );
expectDragStarted( dataTransferMock, trim`
<blockquote>
<p>one</p>
<p>two</p>
<p>three</p>
</blockquote>
` );

fireDragging( dataTransferMock, positionAfterHr );
expectDraggingMarker( positionAfterHr );
} );

it( 'extends nested selection', () => {
setModelData( model, trim`
<blockQuote>
<paragraph>[one</paragraph>
<blockQuote>
<paragraph>two</paragraph>
<paragraph>three</paragraph>
<paragraph>four</paragraph>
</blockQuote>
<paragraph>five]</paragraph>
</blockQuote>
<horizontalLine></horizontalLine>
` );

const dataTransferMock = createDataTransfer();
const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' );

fireDragStart( dataTransferMock );
expectDragStarted( dataTransferMock, trim`
<blockquote>
<p>one</p>
<blockquote>
<p>two</p>
<p>three</p>
<p>four</p>
</blockquote>
<p>five</p>
</blockquote>
` );

fireDragging( dataTransferMock, positionAfterHr );
expectDraggingMarker( positionAfterHr );
} );

it( 'extends selection when it starts at different level than it ends', () => {
setModelData( model, trim`
<blockQuote>
<blockQuote>
<paragraph>[one</paragraph>
<paragraph>two</paragraph>
<paragraph>three</paragraph>
</blockQuote>
<paragraph>four]</paragraph>
</blockQuote>
<horizontalLine></horizontalLine>
` );

const dataTransferMock = createDataTransfer();
const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' );

fireDragStart( dataTransferMock );
expectDragStarted( dataTransferMock, trim`
<blockquote>
<blockquote>
<p>one</p>
<p>two</p>
<p>three</p>
</blockquote>
<p>four</p>
</blockquote>
` );

fireDragging( dataTransferMock, positionAfterHr );
expectDraggingMarker( positionAfterHr );
} );

it( 'extends selection when it ends at different level than it starts', () => {
setModelData( model, trim`
<blockQuote>
<paragraph>[one</paragraph>
<blockQuote>
<paragraph>two</paragraph>
<paragraph>three</paragraph>
<paragraph>four]</paragraph>
</blockQuote>
</blockQuote>
<horizontalLine></horizontalLine>
` );

const dataTransferMock = createDataTransfer();
const positionAfterHr = model.createPositionAt( root.getChild( 1 ), 'after' );

fireDragStart( dataTransferMock );
expectDragStarted( dataTransferMock, trim`
<blockquote>
<p>one</p>
<blockquote>
<p>two</p>
<p>three</p>
<p>four</p>
</blockquote>
</blockquote>
` );

fireDragging( dataTransferMock, positionAfterHr );
expectDraggingMarker( positionAfterHr );
} );
} );
} );

describe( 'integration with the WidgetToolbarRepository plugin', () => {
Expand Down Expand Up @@ -2237,4 +2411,11 @@ describe( 'Drag and Drop experimental', () => {
clientY: y + extraOffset
};
}

function trim( strings ) {
return strings
.join( '' )
.trim()
.replace( />\s+</g, '><' );
}
} );

0 comments on commit 5e26217

Please sign in to comment.