diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index ac798f5433b97c..3c785f00fc610d 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -12,6 +12,7 @@ import {
__experimentalTreeGridRow as TreeGridRow,
MenuGroup,
MenuItem,
+ __unstableUseMotionValue as useMotionValue,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { moreVertical } from '@wordpress/icons';
@@ -48,6 +49,7 @@ export default function ListViewBlock( {
moveItem,
dropItem,
listPosition,
+ parentId,
} ) {
const cellRef = useRef( null );
const [ isHovered, setIsHovered ] = useState( false );
@@ -91,6 +93,7 @@ export default function ListViewBlock( {
clientId,
dropContainer: block?.dropContainer ?? false,
dropSibling: block?.dropSibling ?? false,
+ parentId,
},
} );
}, [ listPosition, draggingId ] );
@@ -167,9 +170,29 @@ export default function ListViewBlock( {
dropItem();
};
+ const velocity = useMotionValue( 0 );
+ const onDrag = ( event, info ) => {
+ // When swapping items with a neighbor a positive translate value is moving down, and a
+ // negative value is moving up in the onViewportBoxUpdate callback.
+ //
+ // However, when skipping over items, we need mouse velocity to understand if the user is dragging up or down.
+ // This is because with the view box in the same position, the originPoint is modified and the translate value
+ // may flip it's sign.
+ //
+ // Velocity is not available in onViewportBoxUpdate, so we set this motion value here:
+ velocity.set( info.velocity.y );
+ };
+
const blockDrag = ( box, delta ) => {
if ( draggingId === clientId ) {
- moveItem( block, listPosition, delta.y );
+ moveItem( {
+ block,
+ translate: delta.y.translate,
+ isLastChild: position === rowCount,
+ isFirstChild: position === 1,
+ velocity,
+ listPosition,
+ } );
}
};
@@ -191,8 +214,10 @@ export default function ListViewBlock( {
drag="y"
whileDrag={ { scale: 1.1 } }
onDragStart={ onDragStart }
+ onDrag={ onDrag }
onDragEnd={ onDragEnd }
onViewportBoxUpdate={ blockDrag }
+ layoutId={ clientId }
>
{ hasNestedBranch && isExpanded && (
{
}
};
-function findCurrentPosition( tree, id, parentId = '' ) {
- for ( let index = 0; index < tree.length; index++ ) {
- const block = tree[ index ];
- if ( block.clientId === id ) {
- return { parentId, index, block, tree };
- }
- if ( block.innerBlocks && block.innerBlocks.length > 0 ) {
- const match = findCurrentPosition(
- block.innerBlocks,
- id,
- block.clientId
- );
- if ( match ) {
- return match;
- }
- }
- }
- return false;
-}
-
-function removeItemFromTree( tree, id ) {
+function removeItemFromTree( tree, id, parentId = '' ) {
const newTree = [];
+ let removeParentId = '';
for ( let index = 0; index < tree.length; index++ ) {
const block = tree[ index ];
if ( block.clientId !== id ) {
if ( block.innerBlocks.length > 0 ) {
+ const {
+ newTree: innerBlocks,
+ removeParentId: cRemoveParentId,
+ } = removeItemFromTree( block.innerBlocks, id, block.clientId );
newTree.push( {
...block,
- innerBlocks: removeItemFromTree( block.innerBlocks, id ),
+ innerBlocks,
} );
+ removeParentId =
+ cRemoveParentId !== '' ? cRemoveParentId : removeParentId;
} else {
newTree.push( { ...block } );
}
+ } else {
+ removeParentId = parentId;
}
}
- return newTree;
+ return { newTree, removeParentId };
}
-function addItemToTree( tree, id, item, insertAfter = true ) {
+function addItemToTree( tree, id, item, insertAfter = true, parentId = '' ) {
const newTree = [];
+ let targetIndex = -1;
+ let targetId = '';
for ( let index = 0; index < tree.length; index++ ) {
const block = tree[ index ];
if ( block.clientId === id ) {
+ targetId = parentId;
if ( insertAfter ) {
+ targetIndex = newTree.length + 1;
newTree.push( { ...block } );
newTree.push( { ...item } );
} else {
+ targetIndex = newTree.length;
newTree.push( { ...item } );
newTree.push( { ...block } );
}
} else if ( block.clientId !== id ) {
if ( block.innerBlocks.length > 0 ) {
+ const {
+ newTree: innerBlocks,
+ targetIndex: childTargetIndex,
+ targetId: childTargetId,
+ } = addItemToTree(
+ block.innerBlocks,
+ id,
+ item,
+ insertAfter,
+ block.clientId
+ );
newTree.push( {
...block,
- innerBlocks: addItemToTree(
- block.innerBlocks,
- id,
- item,
- insertAfter
- ),
+ innerBlocks,
} );
+ targetIndex = Math.max( targetIndex, childTargetIndex );
+ targetId = childTargetId !== '' ? childTargetId : targetId;
} else {
newTree.push( { ...block } );
}
}
}
- return newTree;
+ return { newTree, targetId, targetIndex };
}
+const UP = 'up';
+const DOWN = 'down';
+
// eslint-disable-next-line no-unused-vars
function findFirstValidPosition( positions, current, translate, moveDown ) {
- //TODO: this works, but after skipping an item translate can no longer be used to indicate drag direction.
+ //TODO: add this back when implementing skipping over invalid items
const ITEM_HEIGHT = 36;
const iterate = moveDown ? 1 : -1;
let index = current + iterate;
@@ -211,65 +216,110 @@ export default function ListView( {
lastTarget.current = null;
}, [] );
- const dropItem = () => {
+ const dropItem = async () => {
if ( ! lastTarget.current ) {
return;
}
- const { targetPosition, clientId, movingDown } = lastTarget.current;
- const targetId = targetPosition.clientId;
- const target = findCurrentPosition(
- removeItemFromTree( clientIdsTree, clientId ),
- targetId
- );
- const current = findCurrentPosition( clientIdsTree, clientId );
-
- const targetIndex = movingDown ? target.index + 1 : target.index;
setDropped( true );
- moveBlocksToPosition(
+ const {
+ clientId,
+ originalParent,
+ targetId,
+ targetIndex,
+ } = lastTarget.current;
+ lastTarget.current = null;
+ await moveBlocksToPosition(
[ clientId ],
- current.parentId,
- target.parentId,
+ originalParent,
+ targetId,
targetIndex
);
- lastTarget.current = null;
- // TODO:
- // - use cached representation while list view has focus (maybe after the first drag)
- // - cache removal of the dragged item in tree
- // - try storing parent positions on setPositions
- // - see what performance of a flat representation looks like
+ //TODO: still need to find something more reliable to test if things have settled
timeoutRef.current = setTimeout( () => {
setDropped( false );
}, 200 );
};
- const moveItem = ( block, listPosition, { translate } ) => {
+ const moveItem = ( {
+ block,
+ translate,
+ listPosition,
+ isLastChild,
+ isFirstChild,
+ velocity,
+ } ) => {
//TODO: support add to container
//TODO: support add to child container
//TODO: simplify state and code
+ //TODO: either constrain the drag area to the max number of items, or test if we're hovering over the midpoint of next targets
const { clientId } = block;
const ITEM_HEIGHT = 36;
+ const v = velocity?.get() ?? 0;
+ if ( v === 0 ) {
+ return;
+ }
+
+ const direction = v > 0 ? DOWN : UP;
+
if ( Math.abs( translate ) > ITEM_HEIGHT / 2 ) {
- const movingDown = translate > 0;
- const targetPosition = movingDown
- ? positions[ listPosition + 1 ]
- : positions[ listPosition - 1 ];
+ const position = positions[ listPosition ];
+
+ // First, check to see if we should break out of a container block:
+ if (
+ position.parentId &&
+ ( ( direction === UP && isFirstChild ) ||
+ ( direction === DOWN && isLastChild ) )
+ ) {
+ const {
+ newTree: treeWithoutDragItem,
+ removeParentId,
+ } = removeItemFromTree( clientIdsTree, clientId );
+ const { newTree, targetId, targetIndex } = addItemToTree(
+ treeWithoutDragItem,
+ position.parentId,
+ block,
+ direction === DOWN
+ );
+ lastTarget.current = {
+ clientId,
+ originalParent: removeParentId,
+ targetId,
+ targetIndex,
+ };
+ setTree( newTree );
+ return;
+ }
+
+ // Swap siblings
+ const targetPosition =
+ direction === DOWN
+ ? positions[ listPosition + 1 ]
+ : positions[ listPosition - 1 ];
if ( targetPosition === undefined ) {
return;
}
- lastTarget.current = {
- clientId,
- targetPosition,
- movingDown,
- };
- const newTree = addItemToTree(
- removeItemFromTree( clientIdsTree, clientId ),
- targetPosition.clientId,
- block,
- movingDown
- );
- setTree( newTree );
+ if ( position.parentId === targetPosition.parentId ) {
+ //Sibling swap
+ const {
+ newTree: treeWithoutDragItem,
+ removeParentId,
+ } = removeItemFromTree( clientIdsTree, clientId );
+ const { newTree, targetIndex, targetId } = addItemToTree(
+ treeWithoutDragItem,
+ targetPosition.clientId,
+ block,
+ direction === DOWN
+ );
+ lastTarget.current = {
+ clientId,
+ originalParent: removeParentId,
+ targetId,
+ targetIndex,
+ };
+ setTree( newTree );
+ }
}
};