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
107 changes: 27 additions & 80 deletions packages/block-editor/src/components/use-block-drop-zone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
} );

Expand Down
91 changes: 91 additions & 0 deletions packages/block-editor/src/utils/math.js
Original file line number Diff line number Diff line change
@@ -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 ];
}
122 changes: 122 additions & 0 deletions packages/block-editor/src/utils/test/math.js
Original file line number Diff line number Diff line change
@@ -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' ] );
} );
} );