diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index e04c2cc5bd90a5..7f293844a86145 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -14,16 +14,9 @@ import { useEffect, useState } from '@wordpress/element'; * Internal dependencies */ import useOnBlockDrop from '../use-on-block-drop'; +import { getDistanceToNearestEdge } from '../../utils/math'; -/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ - -/** - * @typedef {Object} WPBlockDragPosition - * @property {number} x The horizontal position of a the block being dragged. - * @property {number} y The vertical position of the block being dragged. - */ - -/** @typedef {import('@wordpress/dom').WPPoint} WPPoint */ +/** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** * The orientation of a block list. @@ -35,93 +28,47 @@ import useOnBlockDrop from '../use-on-block-drop'; * Given a list of block DOM elements finds the index that a block should be dropped * at. * - * This function works for both horizontal and vertical block lists and uses the following - * terms for its variables: - * - * - Lateral, meaning the axis running horizontally when a block list is vertical and vertically when a block list is horizontal. - * - Forward, meaning the axis running vertically when a block list is vertical and horizontally - * when a block list is horizontal. - * - * * @param {Element[]} elements Array of DOM elements that represent each block in a block list. - * @param {WPBlockDragPosition} position The position of the item being dragged. + * @param {WPPoint} position The position of the item being dragged. * @param {WPBlockListOrientation} orientation The orientation of a block list. * * @return {number|undefined} The block index that's closest to the drag position. */ export function getNearestBlockIndex( elements, position, orientation ) { - const { x, y } = position; - const isHorizontal = orientation === 'horizontal'; + const allowedEdges = + orientation === 'horizontal' + ? [ 'left', 'right' ] + : [ 'top', 'bottom' ]; let candidateIndex; let candidateDistance; elements.forEach( ( element, index ) => { const rect = element.getBoundingClientRect(); - const cursorLateralPosition = isHorizontal ? y : x; - const cursorForwardPosition = isHorizontal ? x : y; - const edgeLateralStart = isHorizontal ? rect.top : rect.left; - const edgeLateralEnd = isHorizontal ? rect.bottom : rect.right; - - // When the cursor position is within the lateral bounds of the block, - // measure the straight line distance to the nearest point on the - // block's edge, else measure diagonal distance to the nearest corner. - let edgeLateralPosition; - if ( - cursorLateralPosition >= edgeLateralStart && - cursorLateralPosition <= edgeLateralEnd - ) { - edgeLateralPosition = cursorLateralPosition; - } else if ( cursorLateralPosition < edgeLateralStart ) { - edgeLateralPosition = edgeLateralStart; - } else { - edgeLateralPosition = edgeLateralEnd; - } - const leadingEdgeForwardPosition = isHorizontal ? rect.left : rect.top; - const trailingEdgeForwardPosition = isHorizontal - ? rect.right - : rect.bottom; - - // First measure the distance to the leading edge of the block. - const leadingEdgeDistance = Math.sqrt( - ( cursorLateralPosition - edgeLateralPosition ) ** 2 + - ( cursorForwardPosition - leadingEdgeForwardPosition ) ** 2 - ); - - // If no candidate has been assigned yet or this is the nearest - // block edge to the cursor, then assign it as the candidate. - if ( - candidateDistance === undefined || - Math.abs( leadingEdgeDistance ) < candidateDistance - ) { - candidateDistance = leadingEdgeDistance; - candidateIndex = index; - } - - // Next measure the distance to the trailing edge of the block. - const trailingEdgeDistance = Math.sqrt( - ( cursorLateralPosition - edgeLateralPosition ) ** 2 + - ( cursorForwardPosition - trailingEdgeForwardPosition ) ** 2 + const [ distance, edge ] = getDistanceToNearestEdge( + position, + rect, + allowedEdges ); - // If no candidate has been assigned yet or this is the nearest - // block edge to the cursor, then assign the next block as the candidate. - if ( Math.abs( trailingEdgeDistance ) < candidateDistance ) { - candidateDistance = trailingEdgeDistance; - let nextBlockOffset = 1; - - // If the next block is the one being dragged, skip it and consider - // the block afterwards the drop target. This is needed as the - // block being dragged is set to display: none and won't display - // any drop target styling. - if ( + 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. + const isTrailingEdge = edge === 'bottom' || edge === 'right'; + let offset = isTrailingEdge ? 1 : 0; + + // If the target is the dragged block itself and another 1 to + // index as the dragged block is set to `display: none` and + // should be skipped in the calculation. + const isTargetDraggedBlock = + isTrailingEdge && elements[ index + 1 ] && - elements[ index + 1 ].classList.contains( 'is-dragging' ) - ) { - nextBlockOffset = 2; - } + elements[ index + 1 ].classList.contains( 'is-dragging' ); + offset += isTargetDraggedBlock ? 1 : 0; - candidateIndex = index + nextBlockOffset; + // Update the currently known best candidate. + candidateDistance = distance; + candidateIndex = index + offset; } } ); diff --git a/packages/block-editor/src/utils/math.js b/packages/block-editor/src/utils/math.js new file mode 100644 index 00000000000000..f5140f446c6a12 --- /dev/null +++ b/packages/block-editor/src/utils/math.js @@ -0,0 +1,91 @@ +/** + * A string representing the name of an edge. + * + * @typedef {'top'|'right'|'bottom'|'left'} WPEdgeName + */ + +/** + * @typedef {Object} WPPoint + * @property {number} x The horizontal position. + * @property {number} y The vertical position. + */ + +/** + * Given a point, a DOMRect and the name of an edge, returns the distance to + * that edge of the rect. + * + * This function works for edges that are horizontal or vertical (e.g. not + * rotated), the following terms are used so that the function works in both + * orientations: + * + * - Forward, meaning the axis running horizontally when an edge is vertical + * and vertically when an edge is horizontal. + * - Lateral, meaning the axis running vertically when an edge is vertical + * and horizontally when an edge is horizontal. + * + * @param {WPPoint} point The point to measure distance from. + * @param {DOMRect} rect A DOM Rect containing edge positions. + * @param {WPEdgeName} edge The edge to measure to. + */ +export function getDistanceFromPointToEdge( point, rect, edge ) { + const isHorizontal = edge === 'top' || edge === 'bottom'; + const { x, y } = point; + const pointLateralPosition = isHorizontal ? x : y; + const pointForwardPosition = isHorizontal ? y : x; + const edgeStart = isHorizontal ? rect.left : rect.top; + const edgeEnd = isHorizontal ? rect.right : rect.bottom; + const edgeForwardPosition = rect[ edge ]; + + // Measure the straight line distance to the edge of the rect, when the + // point is adjacent to the edge. + // Else, if the point is positioned diagonally to the edge of the rect, + // measure diagonally to the nearest corner that the edge meets. + let edgeLateralPosition; + if ( + pointLateralPosition >= edgeStart && + pointLateralPosition <= edgeEnd + ) { + edgeLateralPosition = pointLateralPosition; + } else if ( pointLateralPosition < edgeEnd ) { + edgeLateralPosition = edgeStart; + } else { + edgeLateralPosition = edgeEnd; + } + + return Math.sqrt( + ( pointLateralPosition - edgeLateralPosition ) ** 2 + + ( pointForwardPosition - edgeForwardPosition ) ** 2 + ); +} + +/** + * Given a point, a DOMRect and a list of allowed edges returns the name of and + * distance to the nearest edge. + * + * @param {WPPoint} point The point to measure distance from. + * @param {DOMRect} rect A DOM Rect containing edge positions. + * @param {WPEdgeName[]} allowedEdges A list of the edges included in the + * calculation. Defaults to all edges. + * + * @return {[number, string]} An array where the first value is the distance + * and a second is the edge name. + */ +export function getDistanceToNearestEdge( + point, + rect, + allowedEdges = [ 'top', 'bottom', 'left', 'right' ] +) { + let candidateDistance; + let candidateEdge; + + allowedEdges.forEach( ( edge ) => { + const distance = getDistanceFromPointToEdge( point, rect, edge ); + + if ( candidateDistance === undefined || distance < candidateDistance ) { + candidateDistance = distance; + candidateEdge = edge; + } + } ); + + return [ candidateDistance, candidateEdge ]; +} diff --git a/packages/block-editor/src/utils/test/math.js b/packages/block-editor/src/utils/test/math.js new file mode 100644 index 00000000000000..2726e362795440 --- /dev/null +++ b/packages/block-editor/src/utils/test/math.js @@ -0,0 +1,122 @@ +/** + * Internal dependencies + */ +import { getDistanceFromPointToEdge, getDistanceToNearestEdge } from '../math'; + +describe( 'getDistanceFromPointToEdge', () => { + it( 'calculates the horizontal straight line distance when the point is adjacent to a vertical edge', () => { + const point = { x: 0, y: 2 }; + const rect = { + top: 0, + bottom: 4, + left: 2, + }; + expect( getDistanceFromPointToEdge( point, rect, 'left' ) ).toBe( 2 ); + } ); + + it( 'calculates the vertical straight line distance when the point is adjacent to a horizontal edge', () => { + const point = { x: 2, y: 0 }; + const rect = { + top: 2, + left: 0, + right: 4, + }; + expect( getDistanceFromPointToEdge( point, rect, 'top' ) ).toBe( 2 ); + } ); + + it( 'calculates the distance to the nearest corner that the edge forms when the point is not adjacent to a horizontal edge', () => { + const point = { x: 0, y: 0 }; + const rect = { + top: 1, + left: 1, + bottom: 4, + }; + const distance = getDistanceFromPointToEdge( point, rect, 'left' ); + const fixedDistance = distance.toFixed( 2 ); + expect( fixedDistance ).toBe( '1.41' ); + } ); + + it( 'calculates the distance to the nearest corner that the edge forms when the point is not adjacent to a vertical edge', () => { + const point = { x: 0, y: 0 }; + const rect = { + top: 1, + left: 1, + right: 4, + }; + const distance = getDistanceFromPointToEdge( point, rect, 'top' ); + const fixedDistance = distance.toFixed( 2 ); + expect( fixedDistance ).toBe( '1.41' ); + } ); +} ); + +describe( 'getDistanceToNearestEdge', () => { + it( 'returns the correct distance to the top edge, when it is the closest edge', () => { + const point = { x: 3, y: 0 }; + const rect = { + top: 2, + right: 4, + bottom: 4, + left: 2, + }; + expect( getDistanceToNearestEdge( point, rect ) ).toEqual( [ + 2, + 'top', + ] ); + } ); + + it( 'returns the correct distance to the left edge, when it is the closest edge', () => { + const point = { x: 0, y: 3 }; + const rect = { + top: 2, + right: 4, + bottom: 4, + left: 2, + }; + expect( getDistanceToNearestEdge( point, rect ) ).toEqual( [ + 2, + 'left', + ] ); + } ); + + it( 'returns the correct distance to the right edge, when it is the closest edge', () => { + const point = { x: 6, y: 3 }; + const rect = { + top: 2, + right: 4, + bottom: 4, + left: 2, + }; + expect( getDistanceToNearestEdge( point, rect ) ).toEqual( [ + 2, + 'right', + ] ); + } ); + + it( 'returns the correct distance to the bottom edge, when it is the closest edge', () => { + const point = { x: 3, y: 6 }; + const rect = { + top: 2, + right: 4, + bottom: 4, + left: 2, + }; + expect( getDistanceToNearestEdge( point, rect ) ).toEqual( [ + 2, + 'bottom', + ] ); + } ); + + it( 'allows a list of edges to be provided as the third argument', () => { + // Position is closer to right edge, but right edge is not an allowed edge. + const point = { x: 4, y: 2.5 }; + const rect = { + top: 2, + right: 4, + bottom: 4, + left: 2, + }; + expect( + getDistanceToNearestEdge( point, rect, [ 'top', 'bottom' ] ) + ).toEqual( [ 0.5, 'top' ] ); + } ); +} );