diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index a95075c6f9b42c..7cef45fa46206d 100644
--- a/packages/block-editor/src/components/block-list/block.js
+++ b/packages/block-editor/src/components/block-list/block.js
@@ -38,6 +38,8 @@ import { useBlockProps } from './use-block-props';
import { store as blockEditorStore } from '../../store';
import { useLayout } from './layout';
import { BlockListBlockContext } from './block-list-block-context';
+import { DragOnLongPress } from './drag-on-long-press';
+import { __unstableUseBlockRef as useBlockRef } from './use-block-props/use-block-refs';
/**
* Merges wrapper props with special handling for classNames and styles.
@@ -95,18 +97,21 @@ function BlockListBlock( {
themeSupportsLayout,
isTemporarilyEditingAsBlocks,
blockEditingMode,
+ rootClientId,
} = useSelect(
( select ) => {
const {
getSettings,
__unstableGetTemporarilyEditingAsBlocks,
getBlockEditingMode,
+ getBlockRootClientId,
} = select( blockEditorStore );
return {
themeSupportsLayout: getSettings().supportsLayout,
isTemporarilyEditingAsBlocks:
__unstableGetTemporarilyEditingAsBlocks() === clientId,
blockEditingMode: getBlockEditingMode( clientId ),
+ rootClientId: getBlockRootClientId( clientId ),
};
},
[ clientId ]
@@ -229,6 +234,8 @@ function BlockListBlock( {
const memoizedValue = useMemo( () => value, Object.values( value ) );
+ const blockWrapperRef = useBlockRef( clientId );
+
return (
{ block }
+
);
diff --git a/packages/block-editor/src/components/block-list/drag-on-long-press.js b/packages/block-editor/src/components/block-list/drag-on-long-press.js
new file mode 100644
index 00000000000000..9f05a2ccbb521a
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/drag-on-long-press.js
@@ -0,0 +1,149 @@
+/**
+ * WordPress dependencies
+ */
+import { useRef, useEffect, useState, createPortal } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import BlockDraggableChip from '../block-draggable/draggable-chip';
+
+function useOnLongPress( ref, timeout, callback, deps ) {
+ useEffect( () => {
+ let timeoutId;
+ const set = ( event ) => {
+ clearTimeout( timeoutId );
+ timeoutId = setTimeout( () => callback( event ), timeout );
+ };
+ const unset = () => {
+ clearTimeout( timeoutId );
+ };
+
+ ref.current.addEventListener( 'mousedown', set );
+ ref.current.addEventListener( 'mouseup', unset );
+ return () => {
+ ref.current.removeEventListener( 'mousedown', set );
+ ref.current.removeEventListener( 'mouseup', unset );
+ clearTimeout( timeoutId );
+ };
+ }, deps );
+}
+
+export function DragOnLongPress( { target, clientId, rootClientId } ) {
+ const [ isDraggging, setIsDragging ] = useState( false );
+ const container = useRef( document.createElement( 'div' ) );
+
+ useOnLongPress(
+ target,
+ 250,
+ () => {
+ if (
+ // isSelected is oudated.
+ ! target.current.classList.contains( 'is-selected' ) ||
+ ! target.current.ownerDocument.defaultView.getSelection()
+ .isCollapsed
+ ) {
+ return;
+ }
+
+ const cancel = () => {
+ window.removeEventListener( 'mouseup', cancel );
+ document.removeEventListener( 'selectionchange', cancel );
+
+ target.current.style.transform = '';
+ target.current.style.transition = '';
+ };
+
+ window.addEventListener( 'mouseup', cancel );
+ document.addEventListener( 'selectionchange', cancel );
+
+ target.current.style.transform = 'scale(1.02)';
+ // target.current.style.transition = 'transform .75s ease-in-out';
+ },
+ []
+ );
+
+ useOnLongPress(
+ target,
+ 1000,
+ ( _event ) => {
+ target.current.style.transform = '';
+ target.current.style.transition = '';
+
+ if (
+ ! target.current.classList.contains( 'is-selected' ) ||
+ ! target.current.ownerDocument.defaultView.getSelection()
+ .isCollapsed
+ ) {
+ return;
+ }
+
+ const { parentNode } = target.current;
+
+ setIsDragging( true );
+ target.current.style.display = 'none';
+ target.current.ownerDocument.defaultView
+ .getSelection()
+ .removeAllRanges();
+ parentNode.appendChild( container.current );
+ container.current.style.position = 'fixed';
+ container.current.style.pointerEvents = 'none';
+ container.current.style.left = _event.clientX - 20 + 'px';
+ container.current.style.top = _event.clientY + 20 + 'px';
+
+ const onMouseMove = ( event ) => {
+ const newEvent = new window.CustomEvent( 'dragover', {
+ bubbles: true,
+ detail: {
+ clientX: event.clientX,
+ clientY: event.clientY,
+ },
+ } );
+ window.dispatchEvent( newEvent );
+
+ container.current.style.left = event.clientX - 20 + 'px';
+ container.current.style.top = event.clientY + 20 + 'px';
+ };
+
+ const onMouseUp = ( event ) => {
+ window.removeEventListener( 'mousemove', onMouseMove );
+ window.removeEventListener( 'mouseup', onMouseUp );
+
+ setIsDragging( false );
+
+ const dataTransfer = new window.DataTransfer();
+ const data = {
+ type: 'block',
+ srcClientId: clientId,
+ srcRootClientId: rootClientId || '',
+ };
+
+ dataTransfer.setData( 'text', JSON.stringify( data ) );
+
+ const newEvent = new window.DragEvent( 'drop', {
+ bubbles: true,
+ dataTransfer,
+ } );
+ event.target.dispatchEvent( newEvent );
+ parentNode.removeChild( container.current );
+
+ if ( target.current ) {
+ target.current.style.display = '';
+ }
+ };
+
+ window.addEventListener( 'mousemove', onMouseMove );
+ window.addEventListener( 'mouseup', onMouseUp );
+ },
+ []
+ );
+
+ if ( ! isDraggging ) {
+ return null;
+ }
+
+ return createPortal(
+ ,
+ container.current
+ );
+}