@wordpress/block-editor
Version:
208 lines (186 loc) • 5.67 kB
JavaScript
/**
* External dependencies
*/
import {
runOnJS,
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated';
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { useBlockListContext } from '../block-list/block-list-context';
import { getDistanceToNearestEdge } from '../../utils/math';
import useOnBlockDrop from '../use-on-block-drop';
const UPDATE_TARGET_BLOCK_INDEX_THRESHOLD = 20; // In pixels
/** @typedef {import('../../utils/math').WPPoint} WPPoint */
/**
* The orientation of a block list.
*
* @typedef {'horizontal'|'vertical'|undefined} WPBlockListOrientation
*/
/**
* Given a list of blocks layouts finds the index that a block should be dropped at.
*
* @param {Object} blocksLayouts Blocks layouts object.
* @param {WPPoint} position The position of the item being dragged.
* @param {WPBlockListOrientation} orientation The orientation of a block list.
* @param {boolean} isRTL Check if current locale is RTL.
*
* @return {number|undefined} The block index that's closest to the drag position.
*/
export function getNearestBlockIndex(
blocksLayouts,
position,
orientation,
isRTL
) {
const allowedEdges =
orientation === 'horizontal'
? [ 'left', 'right' ]
: [ 'top', 'bottom' ];
const isRightToLeft = isRTL;
let candidateIndex;
let candidateDistance;
// Only enabled for root level blocks.
blocksLayouts.forEach( ( element, index ) => {
const { x, y, width, height } = element;
const rect = {
x: element.x,
y: element.y,
top: y,
right: x + width,
bottom: y + height,
left: x,
width,
height,
};
const [ distance, edge ] = getDistanceToNearestEdge(
position,
rect,
allowedEdges
);
if ( candidateDistance === undefined || distance < candidateDistance ) {
// If the user is dropping to the trailing edge of the block
// add 1 to the index to represent dragging after.
// Take RTL languages into account where the left edge is
// the trailing edge.
const isTrailingEdge =
edge === 'bottom' ||
( ! isRightToLeft && edge === 'right' ) ||
( isRightToLeft && edge === 'left' );
const offset = isTrailingEdge ? 1 : 0;
// Update the currently known best candidate.
candidateDistance = distance;
candidateIndex = index + offset;
}
} );
return candidateIndex;
}
/**
* @typedef {Object} WPBlockDropZoneConfig
* @property {string} rootClientId The root client id for the block list.
*/
/**
* A React hook that can be used to make a block list handle drag and drop.
*
* @param {WPBlockDropZoneConfig} dropZoneConfig configuration data for the drop zone.
*
* @return {Object} An object that contains `targetBlockIndex` and the event
* handlers `onBlockDragOver`, `onBlockDragEnd` and `onBlockDrop`.
*/
export default function useBlockDropZone( {
// An undefined value represents a top-level block. Default to an empty
// string for this so that `targetRootClientId` can be easily compared to
// values returned by the `getRootBlockClientId` selector, which also uses
// an empty string to represent top-level blocks.
rootClientId: targetRootClientId = '',
} = {} ) {
const targetBlockIndex = useSharedValue( null );
const dragPosition = {
x: useSharedValue( 0 ),
y: useSharedValue( 0 ),
};
const prevDragPosition = {
x: useSharedValue( 0 ),
y: useSharedValue( 0 ),
};
const { getBlockListSettings, getSettings } = useSelect( blockEditorStore );
const { blocksLayouts, getBlockLayoutsOrderedByYCoord } =
useBlockListContext();
const getSortedBlocksLayouts = useCallback( () => {
return getBlockLayoutsOrderedByYCoord( blocksLayouts.current );
// We use the value of `blocksLayouts` as the dependency.
}, [ blocksLayouts.current ] );
const isRTL = getSettings().isRTL;
const onBlockDrop = useOnBlockDrop();
const updateTargetBlockIndex = useCallback(
( event ) => {
const sortedBlockLayouts = getSortedBlocksLayouts();
const targetIndex = getNearestBlockIndex(
sortedBlockLayouts,
{ x: event.x, y: event.y },
getBlockListSettings( targetRootClientId )?.orientation,
isRTL
);
if ( targetIndex !== null ) {
targetBlockIndex.value = targetIndex ?? 0;
}
},
[
getSortedBlocksLayouts,
getBlockListSettings,
targetRootClientId,
isRTL,
targetBlockIndex,
]
);
useDerivedValue( () => {
const x = dragPosition.x.value;
const y = dragPosition.y.value;
const prevX = prevDragPosition.x.value;
const prevY = prevDragPosition.y.value;
// `updateTargetBlockIndex` performs expensive calculations, so we throttle
// the call using a offset threshold based on the dragging position.
if (
Math.abs( x - prevX ) >= UPDATE_TARGET_BLOCK_INDEX_THRESHOLD ||
Math.abs( y - prevY ) >= UPDATE_TARGET_BLOCK_INDEX_THRESHOLD
) {
runOnJS( updateTargetBlockIndex )( { x, y } );
prevDragPosition.x.value = x;
prevDragPosition.y.value = y;
return true;
}
return false;
} );
return {
onBlockDragOver( { x, y } ) {
dragPosition.x.value = x;
dragPosition.y.value = y;
},
onBlockDragOverWorklet( { x, y } ) {
'worklet';
dragPosition.x.value = x;
dragPosition.y.value = y;
},
onBlockDragEnd() {
targetBlockIndex.value = null;
},
onBlockDrop: ( event ) => {
if ( targetBlockIndex.value !== null ) {
onBlockDrop( {
...event,
targetRootClientId,
targetBlockIndex: targetBlockIndex.value,
} );
}
},
targetBlockIndex,
};
}