UNPKG

@wordpress/block-editor

Version:
273 lines (242 loc) 8.46 kB
/** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; import { useState, useCallback } from '@wordpress/element'; import { useThrottle, __experimentalUseDropZone as useDropZone, } from '@wordpress/compose'; /** * Internal dependencies */ import { getDistanceToNearestEdge } from '../../utils/math'; import useOnBlockDrop from '../use-on-block-drop'; import { store as blockEditorStore } from '../../store'; /** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** * The type of a drag event. * * @typedef {'default'|'file'|'html'} WPDragEventType */ /** * An array representing data for blocks in the DOM used by drag and drop. * * @typedef {Object} WPBlockNavigationDropZoneBlocks * @property {string} clientId The client id for the block. * @property {string} rootClientId The root client id for the block. * @property {number} blockIndex The block's index. * @property {Element} element The DOM element representing the block. * @property {number} innerBlockCount The number of inner blocks the block has. * @property {boolean} isDraggedBlock Whether the block is currently being dragged. * @property {boolean} canInsertDraggedBlocksAsSibling Whether the dragged block can be a sibling of this block. * @property {boolean} canInsertDraggedBlocksAsChild Whether the dragged block can be a child of this block. */ /** * An object containing details of a drop target. * * @typedef {Object} WPBlockNavigationDropZoneTarget * @property {string} blockIndex The insertion index. * @property {string} rootClientId The root client id for the block. * @property {string|undefined} clientId The client id for the block. * @property {'top'|'bottom'|'inside'} dropPosition The position relative to the block that the user is dropping to. * 'inside' refers to nesting as an inner block. */ /** * Is the point contained by the rectangle. * * @param {WPPoint} point The point. * @param {DOMRect} rect The rectangle. * * @return {boolean} True if the point is contained by the rectangle, false otherwise. */ function isPointContainedByRect( point, rect ) { return ( rect.left <= point.x && rect.right >= point.x && rect.top <= point.y && rect.bottom >= point.y ); } /** * Determines whether the user positioning the dragged block to nest as an * inner block. * * Presently this is determined by whether the cursor is on the right hand side * of the block. * * @param {WPPoint} point The point representing the cursor position when dragging. * @param {DOMRect} rect The rectangle. */ function isNestingGesture( point, rect ) { const blockCenterX = rect.left + rect.width / 2; return point.x > blockCenterX; } // Block navigation is always a vertical list, so only allow dropping // to the above or below a block. const ALLOWED_DROP_EDGES = [ 'top', 'bottom' ]; /** * Given blocks data and the cursor position, compute the drop target. * * @param {WPBlockNavigationDropZoneBlocks} blocksData Data about the blocks in block navigation. * @param {WPPoint} position The point representing the cursor position when dragging. * * @return {WPBlockNavigationDropZoneTarget} An object containing data about the drop target. */ function getBlockNavigationDropTarget( blocksData, position ) { let candidateEdge; let candidateBlockData; let candidateDistance; let candidateRect; for ( const blockData of blocksData ) { if ( blockData.isDraggedBlock ) { continue; } const rect = blockData.element.getBoundingClientRect(); const [ distance, edge ] = getDistanceToNearestEdge( position, rect, ALLOWED_DROP_EDGES ); const isCursorWithinBlock = isPointContainedByRect( position, rect ); if ( candidateDistance === undefined || distance < candidateDistance || isCursorWithinBlock ) { candidateDistance = distance; const index = blocksData.indexOf( blockData ); const previousBlockData = blocksData[ index - 1 ]; // If dragging near the top of a block and the preceding block // is at the same level, use the preceding block as the candidate // instead, as later it makes determining a nesting drop easier. if ( edge === 'top' && previousBlockData && previousBlockData.rootClientId === blockData.rootClientId && ! previousBlockData.isDraggedBlock ) { candidateBlockData = previousBlockData; candidateEdge = 'bottom'; candidateRect = previousBlockData.element.getBoundingClientRect(); } else { candidateBlockData = blockData; candidateEdge = edge; candidateRect = rect; } // If the mouse position is within the block, break early // as the user would intend to drop either before or after // this block. // // This solves an issue where some rows in the block navigation // tree overlap slightly due to sub-pixel rendering. if ( isCursorWithinBlock ) { break; } } } if ( ! candidateBlockData ) { return; } const isDraggingBelow = candidateEdge === 'bottom'; // If the user is dragging towards the bottom of the block check whether // they might be trying to nest the block as a child. // If the block already has inner blocks, this should always be treated // as nesting since the next block in the tree will be the first child. if ( isDraggingBelow && candidateBlockData.canInsertDraggedBlocksAsChild && ( candidateBlockData.innerBlockCount > 0 || isNestingGesture( position, candidateRect ) ) ) { return { rootClientId: candidateBlockData.clientId, blockIndex: 0, dropPosition: 'inside', }; } // If dropping as a sibling, but block cannot be inserted in // this context, return early. if ( ! candidateBlockData.canInsertDraggedBlocksAsSibling ) { return; } const offset = isDraggingBelow ? 1 : 0; return { rootClientId: candidateBlockData.rootClientId, clientId: candidateBlockData.clientId, blockIndex: candidateBlockData.blockIndex + offset, dropPosition: candidateEdge, }; } /** * A react hook for implementing a drop zone in block navigation. * * @return {WPBlockNavigationDropZoneTarget} The drop target. */ export default function useBlockNavigationDropZone() { const { getBlockRootClientId, getBlockIndex, getBlockCount, getDraggedBlockClientIds, canInsertBlocks, } = useSelect( blockEditorStore ); const [ target, setTarget ] = useState(); const { rootClientId: targetRootClientId, blockIndex: targetBlockIndex } = target || {}; const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex ); const throttled = useThrottle( useCallback( ( event, currentTarget ) => { const position = { x: event.clientX, y: event.clientY }; const isBlockDrag = !! event.dataTransfer.getData( 'wp-blocks' ); const draggedBlockClientIds = isBlockDrag ? getDraggedBlockClientIds() : undefined; const blockElements = Array.from( currentTarget.querySelectorAll( '[data-block]' ) ); const blocksData = blockElements.map( ( blockElement ) => { const clientId = blockElement.dataset.block; const rootClientId = getBlockRootClientId( clientId ); return { clientId, rootClientId, blockIndex: getBlockIndex( clientId, rootClientId ), element: blockElement, isDraggedBlock: isBlockDrag ? draggedBlockClientIds.includes( clientId ) : false, innerBlockCount: getBlockCount( clientId ), canInsertDraggedBlocksAsSibling: isBlockDrag ? canInsertBlocks( draggedBlockClientIds, rootClientId ) : true, canInsertDraggedBlocksAsChild: isBlockDrag ? canInsertBlocks( draggedBlockClientIds, clientId ) : true, }; } ); const newTarget = getBlockNavigationDropTarget( blocksData, position ); if ( newTarget ) { setTarget( newTarget ); } }, [] ), 200 ); const ref = useDropZone( { onDrop: onBlockDrop, onDragOver( event ) { // `currentTarget` is only available while the event is being // handled, so get it now and pass it to the thottled function. // https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget throttled( event, event.currentTarget ); }, onDragEnd() { throttled.cancel(); setTarget( null ); }, } ); return { ref, target }; }