diff --git a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts index 621ae9d09fc..f565bd7654e 100644 --- a/packages/ckeditor5-clipboard/src/dragdropexperimental.ts +++ b/packages/ckeditor5-clipboard/src/dragdropexperimental.ts @@ -36,6 +36,7 @@ import { createElement, DomEmitterMixin, delay, + Rect, type DelayedFunc, type ObservableChangeEvent, type DomEmitter @@ -296,7 +297,10 @@ export default class DragDropExperimental extends Plugin { method: 'dragstart' } ); - this._updatePreview( data.dataTransfer ); + const { dataTransfer, domTarget, domEvent } = data; + const { clientX } = domEvent; + + this._updatePreview( { dataTransfer, domTarget, clientX } ); data.stopPropagation(); @@ -621,7 +625,15 @@ export default class DragDropExperimental extends Plugin { /** * Updates the dragged preview image. */ - private _updatePreview( dataTransfer: DataTransfer ): void { + private _updatePreview( { + dataTransfer, + domTarget, + clientX + }: { + dataTransfer: DataTransfer; + domTarget: HTMLElement; + clientX: number; + } ): void { const view = this.editor.editing.view; const editable = view.document.selection.editableElement!; const domEditable = view.domConverter.mapViewToDom( editable )!; @@ -633,18 +645,27 @@ export default class DragDropExperimental extends Plugin { } ); global.document.body.appendChild( this._previewContainer ); - } else { - this._previewContainer.removeChild( this._previewContainer.firstElementChild! ); + } else if ( this._previewContainer.firstElementChild ) { + this._previewContainer.removeChild( this._previewContainer.firstElementChild ); + } + + const domRect = new Rect( domEditable ); + + // If domTarget is inside the editable root, browsers will display the preview correctly by themselves. + if ( domEditable.contains( domTarget ) ) { + return; } + const domEditablePaddingLeft = parseFloat( computedStyle.paddingLeft ); const preview = createElement( global.document, 'div' ); preview.className = 'ck ck-content'; preview.style.width = computedStyle.width; + preview.style.paddingLeft = `${ domRect.left - clientX + domEditablePaddingLeft }px`; preview.innerHTML = dataTransfer.getData( 'text/html' ); + dataTransfer.setDragImage( preview, 0, 0 ); - // TODO set x to make dragged widget stick to the mouse cursor this._previewContainer.appendChild( preview ); } diff --git a/packages/ckeditor5-clipboard/tests/dragdropexperimental.js b/packages/ckeditor5-clipboard/tests/dragdropexperimental.js index 965b10972e8..8ca5bc4574e 100644 --- a/packages/ckeditor5-clipboard/tests/dragdropexperimental.js +++ b/packages/ckeditor5-clipboard/tests/dragdropexperimental.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document, Event */ +/* globals window, document, Event */ import ClipboardPipeline from '../src/clipboardpipeline'; import DragDropExperimental from '../src/dragdropexperimental'; @@ -21,6 +21,7 @@ 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 { Rect } from '@ckeditor/ckeditor5-utils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -1468,7 +1469,7 @@ describe( 'Drag and Drop experimental', () => { expect( editableElement.hasAttribute( 'draggable' ) ).to.be.false; } ); - it( 'should only show one preview element', () => { + it( 'should only show one preview element when you drag element outside the editing root', () => { setModelData( model, '
' + '[foo ' + @@ -1478,10 +1479,13 @@ describe( 'Drag and Drop experimental', () => { '' ); + const pilcrow = document.createElement( 'div' ); + pilcrow.setAttribute( 'class', 'pilcrow' ); + const dataTransferMock = createDataTransfer(); - fireDragStart( dataTransferMock ); - fireDragStart( dataTransferMock ); + fireDragStart( dataTransferMock, () => {}, pilcrow ); + fireDragStart( dataTransferMock, () => {}, pilcrow ); const numberOfCkContentElements = Object .keys( document.getElementsByClassName( 'ck-content' ) ) @@ -1490,6 +1494,66 @@ describe( 'Drag and Drop experimental', () => { // There should be two elements with the `.ck-content` class - editor and drag-and-drop preview. expect( numberOfCkContentElements ).to.equal( 2 ); } ); + + it( 'should show preview with custom implementation if drag element outside the editing root', () => { + setModelData( editor.model, ' [Foo.] ' ); + + const dataTransfer = createDataTransfer( {} ); + + const spy = sinon.spy( dataTransfer, 'setDragImage' ); + const clientX = 10; + + viewDocument.fire( 'dragstart', { + dataTransfer, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domEvent: { + clientX + } + } ); + + const editable = editor.editing.view.document.selection.editableElement; + const domEditable = editor.editing.view.domConverter.mapViewToDom( editable ); + const computedStyle = window.getComputedStyle( domEditable ); + const paddingLeftString = computedStyle.paddingLeft; + const paddingLeft = parseFloat( paddingLeftString ); + + const domRect = new Rect( domEditable ); + + sinon.assert.calledWith( spy, sinon.match( { + style: { + 'padding-left': `${ domRect.left - clientX + paddingLeft }px` + }, + className: 'ck ck-content', + firstChild: sinon.match( { + tagName: 'P', + innerHTML: 'Foo.' + } ) + } ), 0, 0 ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'should show preview with browser implementation if drag element inside the editing root', () => { + setModelData( editor.model, ' [Foo.] ' ); + + const dataTransfer = createDataTransfer( {} ); + + const spy = sinon.spy( dataTransfer, 'setDragImage' ); + + const modelElement = root.getNodeByPath( [ 0 ] ); + const viewElement = mapper.toViewElement( modelElement ); + const domElement = domConverter.mapViewToDom( viewElement ); + + viewDocument.fire( 'dragstart', { + dataTransfer, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( domElement ), + domTarget: domElement + } ); + + sinon.assert.notCalled( spy ); + } ); } ); describe( 'dragenter', () => { @@ -2119,10 +2183,16 @@ describe( 'Drag and Drop experimental', () => { it( 'is enabled when starts dragging the text node', () => { setModelData( editor.model, ' [Foo.] ' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + const dataTransfer = createDataTransfer( {} ); + viewDocument.fire( 'dragstart', { + dataTransfer, preventDefault: sinon.spy(), - dataTransfer: createDataTransfer( {} ), - stopPropagation: sinon.spy() + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.true; @@ -2131,6 +2201,10 @@ describe( 'Drag and Drop experimental', () => { it( 'is disabled when plugin is disabled', () => { setModelData( editor.model, ' Foo. []' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + const plugin = editor.plugins.get( 'DragDropExperimental' ); plugin.isEnabled = false; @@ -2138,7 +2212,8 @@ describe( 'Drag and Drop experimental', () => { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 1 ), dataTransfer: createDataTransfer( {} ), - stopPropagation: sinon.spy() + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -2147,11 +2222,16 @@ describe( 'Drag and Drop experimental', () => { it( 'is disabled when starts dragging the widget', () => { setModelData( editor.model, ' Foo. []' ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 1 ), dataTransfer: createDataTransfer( {} ), - stopPropagation: sinon.spy() + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -2162,11 +2242,16 @@ describe( 'Drag and Drop experimental', () => { const dataTransfer = createDataTransfer( {} ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 0 ), dataTransfer, - stopPropagation: sinon.spy() + stopPropagation: sinon.spy(), + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -2191,11 +2276,16 @@ describe( 'Drag and Drop experimental', () => { const dataTransfer = createDataTransfer( {} ); + const nodeModel = root.getNodeByPath( [ 0 ] ); + const nodeView = mapper.toViewElement( nodeModel ); + const nodeDOM = domConverter.mapViewToDom( nodeView ); + viewDocument.fire( 'dragstart', { preventDefault: sinon.spy(), target: viewDocument.getRoot().getChild( 0 ), dataTransfer, - stopPropagation() {} + stopPropagation() {}, + domEvent: getMockedMousePosition( nodeDOM ) } ); expect( widgetToolbarRepository.isEnabled ).to.be.false; @@ -2272,8 +2362,8 @@ describe( 'Drag and Drop experimental', () => { } ); } ); - function fireDragStart( dataTransferMock, preventDefault = () => {} ) { - const eventData = prepareEventData( model.document.selection.getLastPosition() ); + function fireDragStart( dataTransferMock, preventDefault = () => {}, domTarget ) { + const eventData = prepareEventData( model.document.selection.getLastPosition(), domTarget ); viewDocument.fire( 'mousedown', { ...eventData @@ -2287,8 +2377,6 @@ describe( 'Drag and Drop experimental', () => { } ); } - // ----------------------------------- - function fireDragging( dataTransferMock, modelPositionOrRange ) { viewDocument.fire( 'dragging', { ...prepareEventData( modelPositionOrRange ), @@ -2299,8 +2387,6 @@ describe( 'Drag and Drop experimental', () => { } ); } - // ----------------------------------- - function fireDrop( dataTransferMock, modelPosition ) { viewDocument.fire( 'clipboardInput', { ...prepareEventData( modelPosition ), @@ -2319,7 +2405,7 @@ describe( 'Drag and Drop experimental', () => { } ); } - function prepareEventData( modelPositionOrRange ) { + function prepareEventData( modelPositionOrRange, domTarget ) { let domNode, viewElement, viewRange; if ( modelPositionOrRange.is( 'position' ) ) { @@ -2328,13 +2414,17 @@ describe( 'Drag and Drop experimental', () => { viewRange = view.createRange( viewPosition ); viewElement = mapper.findMappedViewAncestor( viewPosition ); - domNode = viewPosition.parent.is( '$text' ) ? - domConverter.findCorrespondingDomText( viewPosition.parent ).parentNode : - domConverter.mapViewToDom( viewElement ); + if ( !domTarget ) { + domNode = viewPosition.parent.is( '$text' ) ? + domConverter.findCorrespondingDomText( viewPosition.parent ).parentNode : + domConverter.mapViewToDom( viewElement ); + } else { + domNode = domTarget; + } } else { viewRange = mapper.toViewRange( modelPositionOrRange ); viewElement = viewRange.getContainedElement(); - domNode = domConverter.mapViewToDom( viewElement ); + domNode = domTarget || domConverter.mapViewToDom( viewElement ); } return { @@ -2359,8 +2449,6 @@ describe( 'Drag and Drop experimental', () => { } } - // ----------------------------------- - function expectDraggingMarker( targetPositionOrRange ) { expect( model.markers.has( 'drop-target' ) ).to.be.true; @@ -2372,8 +2460,6 @@ describe( 'Drag and Drop experimental', () => { } } - // ----------------------------------- - function expectFinalized() { expect( viewDocument.getRoot().hasAttribute( 'draggable' ) ).to.be.false;