Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const BlockDraggable = ( {
return (
<Draggable
cloneClassname={ cloneClassname }
elementId={ elementId || `block-${ clientIds[ 0 ] }` }
elementId={ elementId }
transferData={ transferData }
onDragStart={ ( event ) => {
startDraggingBlocks( clientIds );
Expand Down
283 changes: 130 additions & 153 deletions packages/components/src/draggable/index.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,39 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, createRef } from '@wordpress/element';
import { withSafeTimeout } from '@wordpress/compose';
import { useEffect, useRef } from '@wordpress/element';

const dragImageClass = 'components-draggable__invisible-drag-image';
const cloneWrapperClass = 'components-draggable__clone';
const cloneHeightTransformationBreakpoint = 700;
const clonePadding = 0;

class Draggable extends Component {
constructor() {
super( ...arguments );

this.onDragStart = this.onDragStart.bind( this );
this.onDragOver = this.onDragOver.bind( this );
this.onDragEnd = this.onDragEnd.bind( this );
this.resetDragState = this.resetDragState.bind( this );
this.dragComponentRef = createRef();
}

componentWillUnmount() {
this.resetDragState();
}
const bodyClass = 'is-dragging-components-draggable';

export default function Draggable( {
children,
onDragStart,
onDragOver,
onDragEnd,
cloneClassname,
elementId,
transferData,
__experimentalDragComponent: dragComponent,
} ) {
const dragComponentRef = useRef();
const cleanup = useRef( () => {} );

/**
* Removes the element clone, resets cursor, and removes drag listener.
*
* @param {Object} event The non-custom DragEvent.
* @param {Object} event The non-custom DragEvent.
*/
onDragEnd( event ) {
const { onDragEnd = noop } = this.props;
function end( event ) {
event.preventDefault();
cleanup.current();

this.resetDragState();

// Allow the Synthetic Event to be accessed from asynchronous code.
// https://reactjs.org/docs/events.html#event-pooling
event.persist();
this.props.setTimeout( onDragEnd.bind( this, event ) );
}

/**
* Updates positioning of element clone based on mouse movement during dragging.
*
* @param {Object} event The non-custom DragEvent.
*/
onDragOver( event ) {
this.cloneWrapper.style.top = `${
parseInt( this.cloneWrapper.style.top, 10 ) +
event.clientY -
this.cursorTop
}px`;
this.cloneWrapper.style.left = `${
parseInt( this.cloneWrapper.style.left, 10 ) +
event.clientX -
this.cursorLeft
}px`;

// Update cursor coordinates.
this.cursorLeft = event.clientX;
this.cursorTop = event.clientY;

const { onDragOver = noop } = this.props;

// The `event` from `onDragOver` is not a SyntheticEvent
// and so it doesn't require `event.persist()`.
this.props.setTimeout( onDragOver.bind( this, event ) );
if ( onDragOver ) {
onDragEnd( event );
}
}

/**
Expand All @@ -82,79 +44,73 @@ class Draggable extends Component {
* - Sets transfer data.
* - Adds dragover listener.
*
* @param {Object} event The non-custom DragEvent.
* @param {Object} event The non-custom DragEvent.
*/
onDragStart( event ) {
const {
cloneClassname,
elementId,
transferData,
onDragStart = noop,
} = this.props;
const element = document.getElementById( elementId );
if ( ! element ) {
event.preventDefault();
return;
}
function start( event ) {
const { ownerDocument } = event.target;

event.dataTransfer.setData( 'text', JSON.stringify( transferData ) );

const cloneWrapper = ownerDocument.createElement( 'div' );
const dragImage = ownerDocument.createElement( 'div' );

// Set a fake drag image to avoid browser defaults. Remove from DOM
// right after. event.dataTransfer.setDragImage is not supported yet in
// IE, we need to check for its existence first.
if ( 'function' === typeof event.dataTransfer.setDragImage ) {
const dragImage = document.createElement( 'div' );
dragImage.id = `drag-image-${ elementId }`;
dragImage.classList.add( dragImageClass );
document.body.appendChild( dragImage );
ownerDocument.body.appendChild( dragImage );
event.dataTransfer.setDragImage( dragImage, 0, 0 );
this.props.setTimeout( () => {
document.body.removeChild( dragImage );
} );
}

event.dataTransfer.setData( 'text', JSON.stringify( transferData ) );
cloneWrapper.classList.add( cloneWrapperClass );

// Prepare element clone and append to element wrapper.
const elementRect = element.getBoundingClientRect();
const elementWrapper = element.parentNode;
const elementTopOffset = parseInt( elementRect.top, 10 );
const elementLeftOffset = parseInt( elementRect.left, 10 );
this.cloneWrapper = document.createElement( 'div' );
this.cloneWrapper.classList.add( cloneWrapperClass );
if ( cloneClassname ) {
this.cloneWrapper.classList.add( cloneClassname );
cloneWrapper.classList.add( cloneClassname );
}

this.cloneWrapper.style.width = `${
elementRect.width + clonePadding * 2
}px`;

// If a dragComponent is defined, the following logic will clone the
// HTML node and inject it into the cloneWrapper.
if ( this.dragComponentRef.current ) {
if ( dragComponentRef.current ) {
// Position dragComponent at the same position as the cursor.
this.cloneWrapper.style.top = `${ event.clientY }px`;
this.cloneWrapper.style.left = `${ event.clientX }px`;
cloneWrapper.style.top = `${ event.clientY }px`;
cloneWrapper.style.left = `${ event.clientX }px`;

const clonedDragComponent = ownerDocument.createElement( 'div' );
clonedDragComponent.innerHTML = dragComponentRef.current.innerHTML;
cloneWrapper.appendChild( clonedDragComponent );

const clonedDragComponent = document.createElement( 'div' );
clonedDragComponent.innerHTML = this.dragComponentRef.current.innerHTML;
this.cloneWrapper.appendChild( clonedDragComponent );
// Inject the cloneWrapper into the DOM.
ownerDocument.body.appendChild( cloneWrapper );
} else {
const element = ownerDocument.getElementById( elementId );

// Prepare element clone and append to element wrapper.
const elementRect = element.getBoundingClientRect();
const elementWrapper = element.parentNode;
const elementTopOffset = parseInt( elementRect.top, 10 );
const elementLeftOffset = parseInt( elementRect.left, 10 );

cloneWrapper.style.width = `${
elementRect.width + clonePadding * 2
}px`;

const clone = element.cloneNode( true );
clone.id = `clone-${ elementId }`;

if ( elementRect.height > cloneHeightTransformationBreakpoint ) {
// Scale down clone if original element is larger than 700px.
this.cloneWrapper.style.transform = 'scale(0.5)';
this.cloneWrapper.style.transformOrigin = 'top left';
cloneWrapper.style.transform = 'scale(0.5)';
cloneWrapper.style.transformOrigin = 'top left';
// Position clone near the cursor.
this.cloneWrapper.style.top = `${ event.clientY - 100 }px`;
this.cloneWrapper.style.left = `${ event.clientX }px`;
cloneWrapper.style.top = `${ event.clientY - 100 }px`;
cloneWrapper.style.left = `${ event.clientX }px`;
} else {
// Position clone right over the original element (20px padding).
this.cloneWrapper.style.top = `${
cloneWrapper.style.top = `${
elementTopOffset - clonePadding
}px`;
this.cloneWrapper.style.left = `${
cloneWrapper.style.left = `${
elementLeftOffset - clonePadding
}px`;
}
Expand All @@ -164,68 +120,89 @@ class Draggable extends Component {
clone.querySelectorAll( 'iframe' )
).forEach( ( child ) => child.parentNode.removeChild( child ) );

this.cloneWrapper.appendChild( clone );
}
cloneWrapper.appendChild( clone );

// Inject the cloneWrapper into the DOM.
elementWrapper.appendChild( this.cloneWrapper );
// Inject the cloneWrapper into the DOM.
elementWrapper.appendChild( cloneWrapper );
}

// Mark the current cursor coordinates.
this.cursorLeft = event.clientX;
this.cursorTop = event.clientY;
let cursorLeft = event.clientX;
let cursorTop = event.clientY;

function over( e ) {
cloneWrapper.style.top = `${
parseInt( cloneWrapper.style.top, 10 ) + e.clientY - cursorTop
}px`;
cloneWrapper.style.left = `${
parseInt( cloneWrapper.style.left, 10 ) + e.clientX - cursorLeft
}px`;

// Update cursor coordinates.
cursorLeft = e.clientX;
cursorTop = e.clientY;

if ( onDragOver ) {
onDragOver( e );
}
}

ownerDocument.addEventListener( 'dragover', over );

// Update cursor to 'grabbing', document wide.
document.body.classList.add( 'is-dragging-components-draggable' );
document.addEventListener( 'dragover', this.onDragOver );
ownerDocument.body.classList.add( bodyClass );

// Allow the Synthetic Event to be accessed from asynchronous code.
// https://reactjs.org/docs/events.html#event-pooling
event.persist();
this.props.setTimeout( onDragStart.bind( this, event ) );
}

/**
* Cleans up drag state when drag has completed, or component unmounts
* while dragging.
*/
resetDragState() {
// Remove drag clone
document.removeEventListener( 'dragover', this.onDragOver );
if ( this.cloneWrapper && this.cloneWrapper.parentNode ) {
this.cloneWrapper.parentNode.removeChild( this.cloneWrapper );
this.cloneWrapper = null;
let timerId;

if ( onDragStart ) {
timerId = setTimeout( () => onDragStart( event ) );
}

this.cursorLeft = null;
this.cursorTop = null;
cleanup.current = () => {
// Remove drag clone
if ( cloneWrapper && cloneWrapper.parentNode ) {
cloneWrapper.parentNode.removeChild( cloneWrapper );
}

// Reset cursor.
document.body.classList.remove( 'is-dragging-components-draggable' );
}
if ( dragImage && dragImage.parentNode ) {
dragImage.parentNode.removeChild( dragImage );
}

// Reset cursor.
ownerDocument.body.classList.remove( bodyClass );

ownerDocument.removeEventListener( 'dragover', over );

render() {
const {
children,
__experimentalDragComponent: dragComponent,
} = this.props;

return (
<>
{ children( {
onDraggableStart: this.onDragStart,
onDraggableEnd: this.onDragEnd,
} ) }
{ dragComponent && (
<div
className="components-draggable-drag-component-root"
style={ { display: 'none' } }
ref={ this.dragComponentRef }
>
{ dragComponent }
</div>
) }
</>
);
clearTimeout( timerId );
};
}
}

export default withSafeTimeout( Draggable );
useEffect(
() => () => {
cleanup.current();
},
[]
);

return (
<>
{ children( {
onDraggableStart: start,
onDraggableEnd: end,
} ) }
{ dragComponent && (
<div
className="components-draggable-drag-component-root"
style={ { display: 'none' } }
ref={ dragComponentRef }
>
{ dragComponent }
</div>
) }
</>
);
}