Skip to content

Commit

Permalink
Hide drop indicator where block isn't allowed to drop. (#56843)
Browse files Browse the repository at this point in the history
* Hide drop indicator where block isn't allowed to drop.

* Don't show indicator when drop outside of allowed parent

* logic to change the draggable icon

* Make chip transparent when dragging over undroppable area.

* Remove unused prop

* Check insertion point visibility

* Move some code around

* Throttle the event listener

* Add dependencies to effect

* Update CSS

* Fade chip only when dragging in editor canvas

* Improve disabled chip

* Update to styled span.

* Only check validity of container blocks.
  • Loading branch information
tellthemachines committed Dec 21, 2023
1 parent e103a18 commit bf87c21
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 13 deletions.
Expand Up @@ -10,7 +10,12 @@ import { dragHandle } from '@wordpress/icons';
*/
import BlockIcon from '../block-icon';

export default function BlockDraggableChip( { count, icon, isPattern } ) {
export default function BlockDraggableChip( {
count,
icon,
isPattern,
fadeWhenDisabled,
} ) {
const patternLabel = isPattern && __( 'Pattern' );
return (
<div className="block-editor-block-draggable-chip-wrapper">
Expand All @@ -37,6 +42,11 @@ export default function BlockDraggableChip( { count, icon, isPattern } ) {
<FlexItem>
<BlockIcon icon={ dragHandle } />
</FlexItem>
{ fadeWhenDisabled && (
<FlexItem className="block-editor-block-draggable-chip__disabled">
<span className="block-editor-block-draggable-chip__disabled-icon"></span>
</FlexItem>
) }
</Flex>
</div>
</div>
Expand Down
120 changes: 116 additions & 4 deletions packages/block-editor/src/components/block-draggable/index.js
Expand Up @@ -5,30 +5,41 @@ import { store as blocksStore } from '@wordpress/blocks';
import { Draggable } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { throttle } from '@wordpress/compose';

/**
* Internal dependencies
*/
import BlockDraggableChip from './draggable-chip';
import useScrollWhenDragging from './use-scroll-when-dragging';
import { store as blockEditorStore } from '../../store';
import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs';
import { isDropTargetValid } from '../use-block-drop-zone';

const BlockDraggable = ( {
children,
clientIds,
cloneClassname,
onDragStart,
onDragEnd,
fadeWhenDisabled = false,
} ) => {
const { srcRootClientId, isDraggable, icon } = useSelect(
const {
srcRootClientId,
isDraggable,
icon,
visibleInserter,
getBlockType,
} = useSelect(
( select ) => {
const {
canMoveBlocks,
getBlockRootClientId,
getBlockName,
getBlockAttributes,
isBlockInsertionPointVisible,
} = select( blockEditorStore );
const { getBlockType, getActiveBlockVariation } =
const { getBlockType: _getBlockType, getActiveBlockVariation } =
select( blocksStore );
const rootClientId = getBlockRootClientId( clientIds[ 0 ] );
const blockName = getBlockName( clientIds[ 0 ] );
Expand All @@ -40,15 +51,21 @@ const BlockDraggable = ( {
return {
srcRootClientId: rootClientId,
isDraggable: canMoveBlocks( clientIds, rootClientId ),
icon: variation?.icon || getBlockType( blockName )?.icon,
icon: variation?.icon || _getBlockType( blockName )?.icon,
visibleInserter: isBlockInsertionPointVisible(),
getBlockType: _getBlockType,
};
},
[ clientIds ]
);

const isDragging = useRef( false );
const [ startScrolling, scrollOnDragOver, stopScrolling ] =
useScrollWhenDragging();

const { getAllowedBlocks, getBlockNamesByClientId, getBlockRootClientId } =
useSelect( blockEditorStore );

const { startDraggingBlocks, stopDraggingBlocks } =
useDispatch( blockEditorStore );

Expand All @@ -61,6 +78,97 @@ const BlockDraggable = ( {
};
}, [] );

// Find the root of the editor iframe.
const blockRef = useBlockRef( clientIds[ 0 ] );
const editorRoot = blockRef.current?.closest( 'body' );

/*
* Add a dragover event listener to the editor root to track the blocks being dragged over.
* The listener has to be inside the editor iframe otherwise the target isn't accessible.
*/
useEffect( () => {
if ( ! editorRoot || ! fadeWhenDisabled ) {
return;
}

const onDragOver = ( event ) => {
if ( ! event.target.closest( '[data-block]' ) ) {
return;
}
const draggedBlockNames = getBlockNamesByClientId( clientIds );
const targetClientId = event.target
.closest( '[data-block]' )
.getAttribute( 'data-block' );

const allowedBlocks = getAllowedBlocks( targetClientId );
const targetBlockName = getBlockNamesByClientId( [
targetClientId,
] )[ 0 ];

/*
* Check if the target is valid to drop in.
* If the target's allowedBlocks is an empty array,
* it isn't a container block, in which case we check
* its parent's validity instead.
*/
let dropTargetValid;
if ( allowedBlocks?.length === 0 ) {
const targetRootClientId =
getBlockRootClientId( targetClientId );
const targetRootBlockName = getBlockNamesByClientId( [
targetRootClientId,
] )[ 0 ];
const rootAllowedBlocks =
getAllowedBlocks( targetRootClientId );
dropTargetValid = isDropTargetValid(
getBlockType,
rootAllowedBlocks,
draggedBlockNames,
targetRootBlockName
);
} else {
dropTargetValid = isDropTargetValid(
getBlockType,
allowedBlocks,
draggedBlockNames,
targetBlockName
);
}

/*
* Update the body class to reflect if drop target is valid.
* This has to be done on the document body because the draggable
* chip is rendered outside of the editor iframe.
*/
if ( ! dropTargetValid && ! visibleInserter ) {
window?.document?.body?.classList?.add(
'block-draggable-invalid-drag-token'
);
} else {
window?.document?.body?.classList?.remove(
'block-draggable-invalid-drag-token'
);
}
};

const throttledOnDragOver = throttle( onDragOver, 200 );

editorRoot.addEventListener( 'dragover', throttledOnDragOver );

return () => {
editorRoot.removeEventListener( 'dragover', throttledOnDragOver );
};
}, [
clientIds,
editorRoot,
fadeWhenDisabled,
getAllowedBlocks,
getBlockNamesByClientId,
getBlockRootClientId,
getBlockType,
visibleInserter,
] );

if ( ! isDraggable ) {
return children( { draggable: false } );
}
Expand Down Expand Up @@ -102,7 +210,11 @@ const BlockDraggable = ( {
}
} }
__experimentalDragComponent={
<BlockDraggableChip count={ clientIds.length } icon={ icon } />
<BlockDraggableChip
count={ clientIds.length }
icon={ icon }
fadeWhenDisabled
/>
}
>
{ ( { onDraggableStart, onDraggableEnd } ) => {
Expand Down
35 changes: 35 additions & 0 deletions packages/block-editor/src/components/block-draggable/style.scss
Expand Up @@ -14,6 +14,7 @@
display: inline-flex;
height: $block-toolbar-height;
padding: 0 ( $grid-unit-15 + $border-width );
position: relative;
user-select: none;
width: max-content;

Expand Down Expand Up @@ -45,3 +46,37 @@
font-size: $default-font-size;
}
}

// Specificity bump to override native component style.
.block-editor-block-draggable-chip__disabled.block-editor-block-draggable-chip__disabled {
opacity: 0;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
transition: all 0.1s linear 0.1s;

.block-editor-block-draggable-chip__disabled-icon {
width: $grid-unit-50 * 0.5;
height: $grid-unit-50 * 0.5;
box-shadow: inset 0 0 0 1.5px $white;
border-radius: 50%;
display: inline-block;
padding: 0;
background: transparent linear-gradient(-45deg, transparent 47.5%, $white 47.5%, $white 52.5%, transparent 52.5%);

}
}

.block-draggable-invalid-drag-token {
.block-editor-block-draggable-chip__disabled.block-editor-block-draggable-chip__disabled {
background-color: $gray-700;
opacity: 1;
box-shadow: 0 4px 8px rgba($black, 0.2);
}
}
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/block-mover/index.js
Expand Up @@ -64,7 +64,7 @@ function BlockMover( { clientIds, hideDragHandle } ) {
} ) }
>
{ ! hideDragHandle && (
<BlockDraggable clientIds={ clientIds }>
<BlockDraggable clientIds={ clientIds } fadeWhenDisabled>
{ ( draggableProps ) => (
<Button
icon={ dragHandle }
Expand Down
89 changes: 82 additions & 7 deletions packages/block-editor/src/components/use-block-drop-zone/index.js
Expand Up @@ -8,7 +8,10 @@ import {
__experimentalUseDropZone as useDropZone,
} from '@wordpress/compose';
import { isRTL } from '@wordpress/i18n';
import { isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock } from '@wordpress/blocks';
import {
isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock,
store as blocksStore,
} from '@wordpress/blocks';

/**
* Internal dependencies
Expand Down Expand Up @@ -191,6 +194,50 @@ export function getDropTargetPosition(
];
}

/**
* Check if the dragged blocks can be dropped on the target.
* @param {Function} getBlockType
* @param {Object[]} allowedBlocks
* @param {string[]} draggedBlockNames
* @param {string} targetBlockName
* @return {boolean} Whether the dragged blocks can be dropped on the target.
*/
export function isDropTargetValid(
getBlockType,
allowedBlocks,
draggedBlockNames,
targetBlockName
) {
// At root level allowedBlocks is undefined and all blocks are allowed.
// Otherwise, check if all dragged blocks are allowed.
let areBlocksAllowed = true;
if ( allowedBlocks ) {
const allowedBlockNames = allowedBlocks?.map( ( { name } ) => name );

areBlocksAllowed = draggedBlockNames.every( ( name ) =>
allowedBlockNames?.includes( name )
);
}

// Work out if dragged blocks have an allowed parent and if so
// check target block matches the allowed parent.
const draggedBlockTypes = draggedBlockNames.map( ( name ) =>
getBlockType( name )
);
const targetMatchesDraggedBlockParents = draggedBlockTypes.every(
( block ) => {
const [ allowedParentName ] = block?.parent || [];
if ( ! allowedParentName ) {
return true;
}

return allowedParentName === targetBlockName;
}
);

return areBlocksAllowed && targetMatchesDraggedBlockParents;
}

/**
* @typedef {Object} WPBlockDropZoneConfig
* @property {?HTMLElement} dropZoneElement Optional element to be used as the drop zone.
Expand Down Expand Up @@ -218,8 +265,15 @@ export default function useBlockDropZone( {
operation: 'insert',
} );

const { getBlockListSettings, getBlocks, getBlockIndex } =
useSelect( blockEditorStore );
const { getBlockType } = useSelect( blocksStore );
const {
getBlockListSettings,
getBlocks,
getBlockIndex,
getDraggedBlockClientIds,
getBlockNamesByClientId,
getAllowedBlocks,
} = useSelect( blockEditorStore );
const { showInsertionPoint, hideInsertionPoint } =
useDispatch( blockEditorStore );

Expand All @@ -235,6 +289,23 @@ export default function useBlockDropZone( {
const throttled = useThrottle(
useCallback(
( event, ownerDocument ) => {
const allowedBlocks = getAllowedBlocks( targetRootClientId );
const targetBlockName = getBlockNamesByClientId( [
targetRootClientId,
] )[ 0 ];
const draggedBlockNames = getBlockNamesByClientId(
getDraggedBlockClientIds()
);
const isBlockDroppingAllowed = isDropTargetValid(
getBlockType,
allowedBlocks,
draggedBlockNames,
targetBlockName
);
if ( ! isBlockDroppingAllowed ) {
return;
}

const blocks = getBlocks( targetRootClientId );

// The block list is empty, don't show the insertion point but still allow dropping.
Expand Down Expand Up @@ -299,14 +370,18 @@ export default function useBlockDropZone( {
} );
},
[
dropZoneElement,
getBlocks,
getAllowedBlocks,
targetRootClientId,
getBlockNamesByClientId,
getDraggedBlockClientIds,
getBlockType,
getBlocks,
getBlockListSettings,
dropZoneElement,
parentBlockClientId,
getBlockIndex,
registry,
showInsertionPoint,
getBlockIndex,
parentBlockClientId,
]
),
200
Expand Down

0 comments on commit bf87c21

Please sign in to comment.