Skip to content

Commit

Permalink
Refactor geometry code out of getNearestBlockIndex (#24715)
Browse files Browse the repository at this point in the history
  • Loading branch information
talldan committed Aug 22, 2020
1 parent d078975 commit d19ec13
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 80 deletions.
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' ] );
} );
} );

0 comments on commit d19ec13

Please sign in to comment.