From eaf97e727a7f42f8ae4cdd8ebf6052c67661f60e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 9 Jun 2020 13:24:44 +0800 Subject: [PATCH 01/10] Use a nearest edge algorithm to detect block drop location --- .../developers/data/data-core-block-editor.md | 27 +++ .../src/components/block-list/index.js | 13 +- .../components/use-block-drop-zone/index.js | 213 ++++++++++++------ packages/block-editor/src/store/actions.js | 19 ++ packages/block-editor/src/store/reducer.js | 21 ++ packages/block-editor/src/store/selectors.js | 12 + 6 files changed, 229 insertions(+), 76 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 04bbcae93c77c..83dd3a47a9a72 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -115,6 +115,19 @@ _Returns_ - `number`: Number of blocks in the post. +# **getBlockDropTarget** + +Returns an object with the root client id and index representing the +position a user is dragging a block over. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether user is dragging blocks. + # **getBlockHierarchyRootClientId** Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. @@ -1246,6 +1259,20 @@ _Parameters_ - _clientId_ `string`: Block client ID. +# **setBlockDropTarget** + +Returns an action object used in signalling that the user is dragging a +block over a particular position in a block list. + +_Parameters_ + +- _rootClientId_ `(undefined|string)`: The clientId representing the block list the block is being hovered over. +- _blockIndex_ `number`: The index representing the position the block is being hovered over. + +_Returns_ + +- `Object`: Action object. + # **setHasControlledInnerBlocks** Returns an action object that sets whether the block has controlled innerblocks. diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index da198a7deba26..6a2d8e080d707 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -37,6 +37,7 @@ function BlockList( function selector( select ) { const { getBlockOrder, + getBlockDropTarget, isMultiSelecting, getSelectedBlockClientId, getMultiSelectedBlockClientIds, @@ -47,6 +48,7 @@ function BlockList( return { blockClientIds: getBlockOrder( rootClientId ), + blockDropTarget: getBlockDropTarget(), isMultiSelecting: isMultiSelecting(), selectedBlockClientId: getSelectedBlockClientId(), multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(), @@ -59,6 +61,7 @@ function BlockList( const { blockClientIds, + blockDropTarget, isMultiSelecting, selectedBlockClientId, multiSelectedBlockClientIds, @@ -67,7 +70,7 @@ function BlockList( } = useSelect( selector, [ rootClientId ] ); const Container = rootClientId ? __experimentalTagName : RootContainer; - const targetClientId = useBlockDropZone( { + useBlockDropZone( { element: ref, rootClientId, } ); @@ -102,7 +105,8 @@ function BlockList( index={ index } enableAnimation={ enableAnimation } className={ - clientId === targetClientId + blockDropTarget.rootClientId === rootClientId && + blockDropTarget.blockIndex === index ? 'is-drop-target' : undefined } @@ -115,7 +119,10 @@ function BlockList( rootClientId={ rootClientId } renderAppender={ renderAppender } className={ - targetClientId === null ? 'is-drop-target' : undefined + blockDropTarget.rootClientId === rootClientId && + blockDropTarget.blockIndex === blockClientIds.length + ? 'is-drop-target' + : undefined } /> 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 6f15cb0284af0..7cdb0bb213a1c 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 @@ -8,7 +8,84 @@ import { findTransform, } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; -import { useEffect, useState, useCallback } from '@wordpress/element'; +import { useEffect, useCallback } from '@wordpress/element'; + +function getNearestBlockIndex( elements, position, orientation ) { + const { x, y } = position; + const isHorizontal = orientation === 'horizontal'; + + let candidateIndex; + let candidateDistance; + + elements.forEach( ( element, index ) => { + // Ensure the element is a block. It should have the `data-block` attribute. + if ( ! element.dataset.block ) { + return; + } + + 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( + Math.pow( cursorLateralPosition - edgeLateralPosition, 2 ) + + Math.pow( + 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( + Math.pow( cursorLateralPosition - edgeLateralPosition, 2 ) + + Math.pow( + cursorForwardPosition - trailingEdgeForwardPosition, + 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 ( Math.abs( trailingEdgeDistance ) < candidateDistance ) { + candidateDistance = trailingEdgeDistance; + candidateIndex = index + 1; + } + } ); + + return candidateIndex; +} const parseDropEvent = ( event ) => { let result = { @@ -34,36 +111,43 @@ const parseDropEvent = ( event ) => { return result; }; -export default function useBlockDropZone( { element, rootClientId } ) { - const [ clientId, setClientId ] = useState( null ); - +export default function useBlockDropZone( { + element, + rootClientId: targetRootClientId, +} ) { function selector( select ) { const { getBlockIndex, + getBlockDropTarget, + getBlockListSettings, getClientIdsOfDescendants, getSettings, getTemplateLock, } = select( 'core/block-editor' ); return { getBlockIndex, - blockIndex: getBlockIndex( clientId, rootClientId ), + targetBlockIndex: getBlockDropTarget().blockIndex, + moverDirection: getBlockListSettings( targetRootClientId ) + ?.__experimentalMoverDirection, getClientIdsOfDescendants, hasUploadPermissions: !! getSettings().mediaUpload, - isLockedAll: getTemplateLock( rootClientId ) === 'all', + isLockedAll: getTemplateLock( targetRootClientId ) === 'all', }; } const { getBlockIndex, - blockIndex, + targetBlockIndex, getClientIdsOfDescendants, hasUploadPermissions, isLockedAll, - } = useSelect( selector, [ rootClientId, clientId ] ); + moverDirection, + } = useSelect( selector, [ targetRootClientId ] ); const { insertBlocks, updateBlockAttributes, moveBlockToPosition, + setBlockDropTarget, } = useDispatch( 'core/block-editor' ); const onFilesDrop = useCallback( @@ -83,15 +167,15 @@ export default function useBlockDropZone( { element, rootClientId } ) { files, updateBlockAttributes ); - insertBlocks( blocks, blockIndex, rootClientId ); + insertBlocks( blocks, targetBlockIndex, targetRootClientId ); } }, [ hasUploadPermissions, updateBlockAttributes, insertBlocks, - blockIndex, - rootClientId, + targetBlockIndex, + targetRootClientId, ] ); @@ -100,73 +184,71 @@ export default function useBlockDropZone( { element, rootClientId } ) { const blocks = pasteHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { - insertBlocks( blocks, blockIndex, rootClientId ); + insertBlocks( blocks, targetBlockIndex, targetRootClientId ); } }, - [ insertBlocks, blockIndex, rootClientId ] + [ insertBlocks, targetBlockIndex, targetRootClientId ] ); const onDrop = useCallback( ( event ) => { const { - srcRootClientId, - srcClientId, - srcIndex, - type, + srcRootClientId: sourceRootClientId, + srcClientId: sourceClientId, + srcIndex: sourceBlockIndex, + type: dropType, } = parseDropEvent( event ); - const isBlockDropType = ( dropType ) => dropType === 'block'; - const isSameLevel = ( srcRoot, dstRoot ) => { - // Note that rootClientId of top-level blocks will be undefined OR a void string, - // so we also need to account for that case separately. - return ( - srcRoot === dstRoot || - ( ! srcRoot === true && ! dstRoot === true ) - ); - }; - const isSameBlock = ( src, dst ) => src === dst; - const isSrcBlockAnAncestorOfDstBlock = ( src, dst ) => - getClientIdsOfDescendants( [ src ] ).some( - ( id ) => id === dst - ); + // If the user isn't dropping a block, return early. + if ( dropType !== 'block' ) { + return; + } + // If the user is dropping to the same position, return early. if ( - ! isBlockDropType( type ) || - isSameBlock( srcClientId, clientId ) || - isSrcBlockAnAncestorOfDstBlock( - srcClientId, - clientId || rootClientId + sourceRootClientId === targetRootClientId && + sourceBlockIndex === targetBlockIndex + ) { + return; + } + + // If the user is attempting to drop a block within its own + // nested blocks, return early as this would create infinite + // recursion. + if ( + targetRootClientId === sourceClientId || + getClientIdsOfDescendants( [ sourceClientId ] ).some( + ( id ) => id === targetRootClientId ) ) { return; } - const dstIndex = clientId - ? getBlockIndex( clientId, rootClientId ) - : undefined; - const positionIndex = blockIndex; + const isAtSameLevel = + sourceRootClientId === targetRootClientId || + ( sourceRootClientId === '' && + targetRootClientId === undefined ); + // If the block is kept at the same level and moved downwards, // subtract to account for blocks shifting upward to occupy its old position. const insertIndex = - dstIndex && - srcIndex < dstIndex && - isSameLevel( srcRootClientId, rootClientId ) - ? positionIndex - 1 - : positionIndex; + isAtSameLevel && sourceBlockIndex < targetBlockIndex + ? targetBlockIndex - 1 + : targetBlockIndex; + moveBlockToPosition( - srcClientId, - srcRootClientId, - rootClientId, + sourceClientId, + sourceRootClientId, + targetRootClientId, insertIndex ); }, [ getClientIdsOfDescendants, getBlockIndex, - clientId, - blockIndex, + targetBlockIndex, moveBlockToPosition, - rootClientId, + targetRootClientId, ] ); @@ -181,33 +263,18 @@ export default function useBlockDropZone( { element, rootClientId } ) { useEffect( () => { if ( position ) { - const { y } = position; - const rect = element.current.getBoundingClientRect(); - - const offset = y - rect.top; - const target = Array.from( element.current.children ).find( - ( blockEl ) => { - return ( - blockEl.offsetTop + blockEl.offsetHeight / 2 > offset - ); - } + const blockElements = Array.from( element.current.children ); + const targetIndex = getNearestBlockIndex( + blockElements, + position, + moverDirection ); - if ( ! target ) { + if ( targetIndex === undefined ) { return; } - const targetClientId = target.id.slice( 'block-'.length ); - - if ( ! targetClientId ) { - return; - } - - setClientId( targetClientId ); + setBlockDropTarget( targetRootClientId, targetIndex ); } }, [ position ] ); - - if ( position ) { - return clientId; - } } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 89ce0d29b1615..9dd21c6ede260 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -707,6 +707,25 @@ export function stopDraggingBlocks() { }; } +/** + * Returns an action object used in signalling that the user is dragging a + * block over a particular position in a block list. + * + * @param {undefined|string} rootClientId The clientId representing the block + * list the block is being hovered over. + * @param {number} blockIndex The index representing the position + * the block is being hovered over. + * + * @return {Object} Action object. + */ +export function setBlockDropTarget( rootClientId, blockIndex ) { + return { + type: 'SET_BLOCK_DROP_TARGET', + rootClientId, + blockIndex, + }; +} + /** * Returns an action object used in signalling that the caret has entered formatted text. * diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 38e6bbde0d7c9..3c3729b1cf4ac 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1146,6 +1146,26 @@ export function isDraggingBlocks( state = false, action ) { return state; } +/** + * + * @param {*} state + * @param {*} action + */ +export function blockDropTarget( state = {}, action ) { + switch ( action.type ) { + case 'SET_BLOCK_DROP_TARGET': + const { rootClientId, blockIndex } = action; + return { + rootClientId, + blockIndex, + }; + case 'STOP_DRAGGING_BLOCKS': + return {}; + } + + return state; +} + /** * Reducer returning whether the caret is within formatted text. * @@ -1625,6 +1645,7 @@ export default combineReducers( { blocks, isTyping, isDraggingBlocks, + blockDropTarget, isCaretWithinFormattedText, selectionStart, selectionEnd, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index d2b4a86dd13a3..51e3efe508593 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1011,6 +1011,18 @@ export function isDraggingBlocks( state ) { return state.isDraggingBlocks; } +/** + * Returns an object with the root client id and index representing the + * position a user is dragging a block over. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether user is dragging blocks. + */ +export function getBlockDropTarget( state ) { + return state.blockDropTarget; +} + /** * Returns true if the caret is within formatted text, or false otherwise. * From 7106ae553bf178544488cfbb7366d7f3545404fe Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 9 Jun 2020 15:43:25 +0800 Subject: [PATCH 02/10] Try adding a drop target style for the block list appender --- .../src/components/block-list-appender/style.scss | 11 ----------- .../block-editor/src/components/block-list/style.scss | 6 +++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/components/block-list-appender/style.scss b/packages/block-editor/src/components/block-list-appender/style.scss index e382d17507063..f5eb38c57f8f4 100644 --- a/packages/block-editor/src/components/block-list-appender/style.scss +++ b/packages/block-editor/src/components/block-list-appender/style.scss @@ -19,17 +19,6 @@ } } -.block-list-appender.is-drop-target > div::before { - content: ""; - position: absolute; - right: -$grid-unit-10; - left: -$grid-unit-10; - top: -$grid-unit-10; - bottom: -$grid-unit-10; - border-radius: $radius-block-ui; - border: 3px solid var(--wp-admin-theme-color); -} - .block-list-appender > .block-editor-inserter { display: block; } diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 0ffd059aacdfb..18a614b9f8075 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -96,6 +96,11 @@ opacity: 1; } } +} + +.block-editor-block-list__layout .block-editor-block-list__block, +.block-editor-block-list__layout .block-list-appender { + position: relative; // Between-blocks dropzone line indicator. &.is-drop-target::before { @@ -111,7 +116,6 @@ } } - /** * Cross-Block Selection */ From b7547a276e7764fb358f7754d58b2244e9751284 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 9 Jun 2020 17:30:07 +0800 Subject: [PATCH 03/10] Add horizontal block drop indication --- .../src/components/block-list/index.js | 35 ++++++++++++------- .../src/components/block-list/style.scss | 11 +++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 6a2d8e080d707..f33bdfdbd6eff 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -38,6 +38,7 @@ function BlockList( const { getBlockOrder, getBlockDropTarget, + getBlockListSettings, isMultiSelecting, getSelectedBlockClientId, getMultiSelectedBlockClientIds, @@ -52,6 +53,8 @@ function BlockList( isMultiSelecting: isMultiSelecting(), selectedBlockClientId: getSelectedBlockClientId(), multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(), + moverDirection: getBlockListSettings( rootClientId ) + ?.__experimentalMoverDirection, hasMultiSelection: hasMultiSelection(), enableAnimation: ! isTyping() && @@ -65,6 +68,7 @@ function BlockList( isMultiSelecting, selectedBlockClientId, multiSelectedBlockClientIds, + moverDirection, hasMultiSelection, enableAnimation, } = useSelect( selector, [ rootClientId ] ); @@ -75,6 +79,10 @@ function BlockList( rootClientId, } ); + const isAppenderDropTarget = + blockDropTarget.rootClientId === rootClientId && + blockDropTarget.blockIndex === blockClientIds.length; + return ( ); @@ -118,12 +130,11 @@ function BlockList( tagName={ __experimentalAppenderTagName } rootClientId={ rootClientId } renderAppender={ renderAppender } - className={ - blockDropTarget.rootClientId === rootClientId && - blockDropTarget.blockIndex === blockClientIds.length - ? 'is-drop-target' - : undefined - } + className={ classnames( { + 'is-drop-target': isAppenderDropTarget, + 'is-dropping-horizontally': + isAppenderDropTarget && moverDirection === 'horizontal', + } ) } /> ); diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 18a614b9f8075..932115b43f760 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -109,11 +109,20 @@ z-index: 0; pointer-events: none; transition: border-color 0.1s linear, border-style 0.1s linear, box-shadow 0.1s linear; + top: -$default-block-margin / 2; right: 0; left: 0; - top: -$default-block-margin / 2; border-top: 4px solid var(--wp-admin-theme-color); } + + &.is-drop-target.is-dropping-horizontally::before { + top: 0; + bottom: 0; + // Drop target border-width plus a couple of pixels so that the border looks between blocks. + left: -6px; + border-top: none; + border-left: 4px solid var(--wp-admin-theme-color); + } } /** From 92f2bb81d2ace75abd0f56d2c965c5d413256b28 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 10 Jun 2020 15:24:24 +0800 Subject: [PATCH 04/10] Revert store changes and instead use value returned from useBlockDropZone hook --- .../src/components/block-list/index.js | 13 +++--------- .../components/use-block-drop-zone/index.js | 14 +++++++------ packages/block-editor/src/store/actions.js | 19 ----------------- packages/block-editor/src/store/reducer.js | 21 ------------------- packages/block-editor/src/store/selectors.js | 12 ----------- 5 files changed, 11 insertions(+), 68 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index f33bdfdbd6eff..0f60351182632 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -37,7 +37,6 @@ function BlockList( function selector( select ) { const { getBlockOrder, - getBlockDropTarget, getBlockListSettings, isMultiSelecting, getSelectedBlockClientId, @@ -49,7 +48,6 @@ function BlockList( return { blockClientIds: getBlockOrder( rootClientId ), - blockDropTarget: getBlockDropTarget(), isMultiSelecting: isMultiSelecting(), selectedBlockClientId: getSelectedBlockClientId(), multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(), @@ -64,7 +62,6 @@ function BlockList( const { blockClientIds, - blockDropTarget, isMultiSelecting, selectedBlockClientId, multiSelectedBlockClientIds, @@ -74,14 +71,12 @@ function BlockList( } = useSelect( selector, [ rootClientId ] ); const Container = rootClientId ? __experimentalTagName : RootContainer; - useBlockDropZone( { + const dropTargetIndex = useBlockDropZone( { element: ref, rootClientId, } ); - const isAppenderDropTarget = - blockDropTarget.rootClientId === rootClientId && - blockDropTarget.blockIndex === blockClientIds.length; + const isAppenderDropTarget = dropTargetIndex === blockClientIds.length; return ( Date: Wed, 10 Jun 2020 16:59:43 +0800 Subject: [PATCH 05/10] Remove docs change --- .../developers/data/data-core-block-editor.md | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 83dd3a47a9a72..04bbcae93c77c 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -115,19 +115,6 @@ _Returns_ - `number`: Number of blocks in the post. -# **getBlockDropTarget** - -Returns an object with the root client id and index representing the -position a user is dragging a block over. - -_Parameters_ - -- _state_ `Object`: Global application state. - -_Returns_ - -- `boolean`: Whether user is dragging blocks. - # **getBlockHierarchyRootClientId** Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. @@ -1259,20 +1246,6 @@ _Parameters_ - _clientId_ `string`: Block client ID. -# **setBlockDropTarget** - -Returns an action object used in signalling that the user is dragging a -block over a particular position in a block list. - -_Parameters_ - -- _rootClientId_ `(undefined|string)`: The clientId representing the block list the block is being hovered over. -- _blockIndex_ `number`: The index representing the position the block is being hovered over. - -_Returns_ - -- `Object`: Action object. - # **setHasControlledInnerBlocks** Returns an action object that sets whether the block has controlled innerblocks. From f7f3a37940fbdcdf9872efbbe21e004ea24f5dd4 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 11 Jun 2020 16:59:20 +0800 Subject: [PATCH 06/10] Add JSDocs to useBlockDropZone --- .../components/use-block-drop-zone/index.js | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) 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 d5510be1bd88f..03f95bbc9af47 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 @@ -10,6 +10,38 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect, useCallback, useState } from '@wordpress/element'; +/** @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. + */ + +/** + * The orientation of a block list. + * + * @typedef {'horizontal'|'vertical'|undefined} WPBlockListOrientation + */ + +/** + * 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 or vice-versa. + * - 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 {WPBlockListOrientation} orientation The orientation of a block list. + * + * @return {number|undefined} The block index that's closest to the drag position. + */ function getNearestBlockIndex( elements, position, orientation ) { const { x, y } = position; const isHorizontal = orientation === 'horizontal'; @@ -87,7 +119,14 @@ function getNearestBlockIndex( elements, position, orientation ) { return candidateIndex; } -const parseDropEvent = ( event ) => { +/** + * Retrieve the data for a block drop event. + * + * @param {WPSyntheticEvent} event The drop event. + * + * @return {Object} An object with block drag and drop data. + */ +function parseDropEvent( event ) { let result = { srcRootClientId: null, srcClientId: null, @@ -109,8 +148,21 @@ const parseDropEvent = ( event ) => { } return result; -}; +} + +/** + * @typedef {Object} WPBlockDropZoneConfig + * @property {Object} element A React ref object pointing to the block list's DOM element. + * @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 {number|undefined} The block index that's closest to the drag position. + */ export default function useBlockDropZone( { element, rootClientId: targetRootClientId, From 25fe575ace1f590ac2703f17e5db40f044911afa Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 11 Jun 2020 17:37:49 +0800 Subject: [PATCH 07/10] Add tests for getNearestBlockIndex --- .../components/use-block-drop-zone/index.js | 2 +- .../use-block-drop-zone/test/index.js | 314 ++++++++++++++++++ 2 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 packages/block-editor/src/components/use-block-drop-zone/test/index.js 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 03f95bbc9af47..6bb1fbc5e97a8 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 @@ -42,7 +42,7 @@ import { useEffect, useCallback, useState } from '@wordpress/element'; * * @return {number|undefined} The block index that's closest to the drag position. */ -function getNearestBlockIndex( elements, position, orientation ) { +export function getNearestBlockIndex( elements, position, orientation ) { const { x, y } = position; const isHorizontal = orientation === 'horizontal'; diff --git a/packages/block-editor/src/components/use-block-drop-zone/test/index.js b/packages/block-editor/src/components/use-block-drop-zone/test/index.js new file mode 100644 index 0000000000000..a135f18285ebe --- /dev/null +++ b/packages/block-editor/src/components/use-block-drop-zone/test/index.js @@ -0,0 +1,314 @@ +/** + * Internal dependencies + */ +import { getNearestBlockIndex } from '..'; + +const elementData = [ + { + top: 0, + left: 0, + bottom: 200, + right: 400, + }, + { + top: 200, + left: 0, + bottom: 500, + right: 400, + }, + { + top: 500, + left: 0, + bottom: 900, + right: 400, + }, + // Fourth block wraps to the next row/column + { + top: 0, + left: 400, + bottom: 300, + right: 800, + }, +]; + +const mapElements = ( orientation ) => ( + { top, right, bottom, left }, + index +) => { + return { + dataset: { block: index + 1 }, + getBoundingClientRect() { + return orientation === 'vertical' + ? { + top, + right, + bottom, + left, + } + : { + top: left, + bottom: right, + left: top, + right: bottom, + }; + }, + }; +}; + +const verticalElements = elementData.map( mapElements( 'vertical' ) ); +// Flip the elementData to make a horizontal block list. +const horizontalElements = elementData.map( mapElements( 'horizontal' ) ); + +describe( 'getNearestBlockIndex', () => { + it( 'returns `undefined` for an empty list of elements', () => { + const emptyElementList = []; + const position = { x: 0, y: 0 }; + const orientation = 'horizontal'; + + const result = getNearestBlockIndex( + emptyElementList, + position, + orientation + ); + + expect( result ).toBeUndefined(); + } ); + + it( 'returns `undefined` if the elements do not have the `data-block` attribute', () => { + const nonBlockElements = [ { dataset: {} } ]; + const position = { x: 0, y: 0 }; + const orientation = 'horizontal'; + + const result = getNearestBlockIndex( + nonBlockElements, + position, + orientation + ); + + expect( result ).toBeUndefined(); + } ); + + describe( 'Vertical block lists', () => { + const orientation = 'vertical'; + + it( 'returns `0` when the position is nearest to the start of the first block', () => { + const position = { x: 0, y: 0 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 0 ); + } ); + + it( 'returns `1` when the position is nearest to the end of the first block', () => { + const position = { x: 0, y: 190 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 1 ); + } ); + + it( 'returns `1` when the position is nearest to the start of the second block', () => { + const position = { x: 0, y: 210 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 1 ); + } ); + + it( 'returns `2` when the position is nearest to the end of the second block', () => { + const position = { x: 0, y: 450 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 2 ); + } ); + + it( 'returns `2` when the position is nearest to the start of the third block', () => { + const position = { x: 0, y: 510 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 2 ); + } ); + + it( 'returns `3` when the position is nearest to the end of the third block', () => { + const position = { x: 0, y: 880 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `3` when the position is past the end of the third block', () => { + const position = { x: 0, y: 920 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `3` when the position is nearest to the start of the fourth block', () => { + const position = { x: 401, y: 0 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `4` when the position is nearest to the end of the fourth block', () => { + const position = { x: 401, y: 300 }; + + const result = getNearestBlockIndex( + verticalElements, + position, + orientation + ); + + expect( result ).toBe( 4 ); + } ); + } ); + + describe( 'Horizontal block lists', () => { + const orientation = 'horizontal'; + + it( 'returns `0` when the position is nearest to the start of the first block', () => { + const position = { x: 0, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 0 ); + } ); + + it( 'returns `1` when the position is nearest to the end of the first block', () => { + const position = { x: 190, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 1 ); + } ); + + it( 'returns `1` when the position is nearest to the start of the second block', () => { + const position = { x: 210, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 1 ); + } ); + + it( 'returns `2` when the position is nearest to the end of the second block', () => { + const position = { x: 450, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 2 ); + } ); + + it( 'returns `2` when the position is nearest to the start of the third block', () => { + const position = { x: 510, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 2 ); + } ); + + it( 'returns `3` when the position is nearest to the end of the third block', () => { + const position = { x: 880, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `3` when the position is past the end of the third block', () => { + const position = { x: 920, y: 0 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `3` when the position is nearest to the start of the fourth block', () => { + const position = { x: 0, y: 401 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 3 ); + } ); + + it( 'returns `4` when the position is nearest to the end of the fourth block', () => { + const position = { x: 300, y: 401 }; + + const result = getNearestBlockIndex( + horizontalElements, + position, + orientation + ); + + expect( result ).toBe( 4 ); + } ); + } ); +} ); From a293a9748659f67e29a6f6552d92d41f3c2fdfd5 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 17 Jun 2020 13:08:24 +0800 Subject: [PATCH 08/10] Update doc block comment Co-authored-by: Robert Anderson --- .../block-editor/src/components/use-block-drop-zone/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6bb1fbc5e97a8..ab2dee1571d25 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 @@ -31,7 +31,7 @@ import { useEffect, useCallback, useState } from '@wordpress/element'; * 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 or vice-versa. + * - 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. * From 92719c7215bf187af88bc125402a3bef23d77c7f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 17 Jun 2020 13:23:46 +0800 Subject: [PATCH 09/10] Use ** instead of Math.pow to make the code look pretty --- .../src/components/use-block-drop-zone/index.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 ab2dee1571d25..63be0df0123a3 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 @@ -82,11 +82,8 @@ export function getNearestBlockIndex( elements, position, orientation ) { // First measure the distance to the leading edge of the block. const leadingEdgeDistance = Math.sqrt( - Math.pow( cursorLateralPosition - edgeLateralPosition, 2 ) + - Math.pow( - cursorForwardPosition - leadingEdgeForwardPosition, - 2 - ) + ( cursorLateralPosition - edgeLateralPosition ) ** 2 + + ( cursorForwardPosition - leadingEdgeForwardPosition ) ** 2 ); // If no candidate has been assigned yet or this is the nearest @@ -101,11 +98,8 @@ export function getNearestBlockIndex( elements, position, orientation ) { // Next measure the distance to the trailing edge of the block. const trailingEdgeDistance = Math.sqrt( - Math.pow( cursorLateralPosition - edgeLateralPosition, 2 ) + - Math.pow( - cursorForwardPosition - trailingEdgeForwardPosition, - 2 - ) + ( cursorLateralPosition - edgeLateralPosition ) ** 2 + + ( cursorForwardPosition - trailingEdgeForwardPosition ** 2 ) ); // If no candidate has been assigned yet or this is the nearest From aa1dc17aba2da0733ff5c0bc902b631699519147 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 17 Jun 2020 13:49:10 +0800 Subject: [PATCH 10/10] Fix incorrect calculation --- .../block-editor/src/components/use-block-drop-zone/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 63be0df0123a3..77861a19d63f6 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 @@ -99,7 +99,7 @@ export function getNearestBlockIndex( elements, position, orientation ) { // Next measure the distance to the trailing edge of the block. const trailingEdgeDistance = Math.sqrt( ( cursorLateralPosition - edgeLateralPosition ) ** 2 + - ( cursorForwardPosition - trailingEdgeForwardPosition ** 2 ) + ( cursorForwardPosition - trailingEdgeForwardPosition ) ** 2 ); // If no candidate has been assigned yet or this is the nearest