diff --git a/packages/block-editor/src/components/block-drop-zone/index.js b/packages/block-editor/src/components/block-drop-zone/index.js index e3c93300790f5..d78cb7521a4bc 100644 --- a/packages/block-editor/src/components/block-drop-zone/index.js +++ b/packages/block-editor/src/components/block-drop-zone/index.js @@ -1,23 +1,14 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { - DropZone, - withFilters, -} from '@wordpress/components'; +import { __unstableUseDropZone as useDropZone } from '@wordpress/components'; import { pasteHandler, getBlockTransforms, findTransform, } from '@wordpress/blocks'; -import { Component } from '@wordpress/element'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect, useState, useCallback } from '@wordpress/element'; const parseDropEvent = ( event ) => { let result = { @@ -40,49 +31,63 @@ const parseDropEvent = ( event ) => { return result; }; -class BlockDropZone extends Component { - constructor() { - super( ...arguments ); - - this.onFilesDrop = this.onFilesDrop.bind( this ); - this.onHTMLDrop = this.onHTMLDrop.bind( this ); - this.onDrop = this.onDrop.bind( this ); - } +export default function useBlockDropZone( { element, rootClientId } ) { + const [ clientId, setClientId ] = useState( null ); - getInsertIndex( position ) { - const { clientId, rootClientId, getBlockIndex } = this.props; - if ( clientId !== undefined ) { - const index = getBlockIndex( clientId, rootClientId ); - return ( position && position.y === 'top' ) ? index : index + 1; - } + function selector( select ) { + const { + getBlockIndex, + getClientIdsOfDescendants, + getSettings, + getTemplateLock, + } = select( 'core/block-editor' ); + return { + getBlockIndex, + blockIndex: getBlockIndex( clientId, rootClientId ), + getClientIdsOfDescendants, + hasUploadPermissions: !! getSettings().mediaUpload, + isLockedAll: getTemplateLock( rootClientId ) === 'all', + }; } - onFilesDrop( files, position ) { - if ( ! this.props.hasUploadPermissions ) { + const { + getBlockIndex, + blockIndex, + getClientIdsOfDescendants, + hasUploadPermissions, + isLockedAll, + } = useSelect( selector, [ rootClientId, clientId ] ); + const { + insertBlocks, + updateBlockAttributes, + moveBlockToPosition, + } = useDispatch( 'core/block-editor' ); + + const onFilesDrop = useCallback( ( files ) => { + if ( ! hasUploadPermissions ) { return; } + const transformation = findTransform( getBlockTransforms( 'from' ), ( transform ) => transform.type === 'files' && transform.isMatch( files ) ); if ( transformation ) { - const insertIndex = this.getInsertIndex( position ); - const blocks = transformation.transform( files, this.props.updateBlockAttributes ); - this.props.insertBlocks( blocks, insertIndex ); + const blocks = transformation.transform( files, updateBlockAttributes ); + insertBlocks( blocks, blockIndex, rootClientId ); } - } + }, [ hasUploadPermissions, updateBlockAttributes, insertBlocks, blockIndex, rootClientId ] ); - onHTMLDrop( HTML, position ) { + const onHTMLDrop = useCallback( ( HTML ) => { const blocks = pasteHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { - this.props.insertBlocks( blocks, this.getInsertIndex( position ) ); + insertBlocks( blocks, blockIndex, rootClientId ); } - } + }, [ insertBlocks, blockIndex, rootClientId ] ); - onDrop( event, position ) { - const { rootClientId: dstRootClientId, clientId: dstClientId, getClientIdsOfDescendants, getBlockIndex } = this.props; + const onDrop = useCallback( ( event ) => { const { srcRootClientId, srcClientId, srcIndex, type } = parseDropEvent( event ); const isBlockDropType = ( dropType ) => dropType === 'block'; @@ -95,75 +100,53 @@ class BlockDropZone extends Component { const isSrcBlockAnAncestorOfDstBlock = ( src, dst ) => getClientIdsOfDescendants( [ src ] ).some( ( id ) => id === dst ); if ( ! isBlockDropType( type ) || - isSameBlock( srcClientId, dstClientId ) || - isSrcBlockAnAncestorOfDstBlock( srcClientId, dstClientId || dstRootClientId ) ) { + isSameBlock( srcClientId, clientId ) || + isSrcBlockAnAncestorOfDstBlock( srcClientId, clientId || rootClientId ) ) { return; } - const dstIndex = dstClientId ? getBlockIndex( dstClientId, dstRootClientId ) : undefined; - const positionIndex = this.getInsertIndex( position ); + const dstIndex = clientId ? getBlockIndex( clientId, rootClientId ) : undefined; + const positionIndex = blockIndex; // 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, dstRootClientId ) ? positionIndex - 1 : positionIndex; - this.props.moveBlockToPosition( srcClientId, srcRootClientId, insertIndex ); - } - - render() { - const { hasUploadPermissions, isLockedAll } = this.props; - if ( isLockedAll ) { - return null; + const insertIndex = dstIndex && srcIndex < dstIndex && isSameLevel( srcRootClientId, rootClientId ) ? positionIndex - 1 : positionIndex; + moveBlockToPosition( srcClientId, srcRootClientId, rootClientId, insertIndex ); + }, [ getClientIdsOfDescendants, getBlockIndex, clientId, blockIndex, moveBlockToPosition, rootClientId ] ); + + const { position } = useDropZone( { + element, + onFilesDrop, + onHTMLDrop, + onDrop, + isDisabled: isLockedAll, + withPosition: true, + } ); + + 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; + } ); + + if ( ! target ) { + return; + } + + const targetClientId = target.id.slice( 'block-'.length ); + + if ( ! targetClientId ) { + return; + } + + setClientId( targetClientId ); } - const index = this.getInsertIndex(); - const isAppender = index === undefined; - return ( - - ); + }, [ position ] ); + + if ( position ) { + return clientId; } } - -export default compose( - withDispatch( ( dispatch, ownProps ) => { - const { - insertBlocks, - updateBlockAttributes, - moveBlockToPosition, - } = dispatch( 'core/block-editor' ); - - return { - insertBlocks( blocks, index ) { - const { rootClientId } = ownProps; - - insertBlocks( blocks, index, rootClientId ); - }, - updateBlockAttributes( ...args ) { - updateBlockAttributes( ...args ); - }, - moveBlockToPosition( srcClientId, srcRootClientId, dstIndex ) { - const { rootClientId: dstRootClientId } = ownProps; - moveBlockToPosition( srcClientId, srcRootClientId, dstRootClientId, dstIndex ); - }, - }; - } ), - withSelect( ( select, { rootClientId } ) => { - const { - getBlockIndex, - getClientIdsOfDescendants, - getSettings, - getTemplateLock, - } = select( 'core/block-editor' ); - return { - getBlockIndex, - getClientIdsOfDescendants, - hasUploadPermissions: !! getSettings().mediaUpload, - isLockedAll: getTemplateLock( rootClientId ) === 'all', - }; - } ), - withFilters( 'editor.BlockDropZone' ) -)( BlockDropZone ); diff --git a/packages/block-editor/src/components/block-drop-zone/style.scss b/packages/block-editor/src/components/block-drop-zone/style.scss deleted file mode 100644 index 8135c66f32ffa..0000000000000 --- a/packages/block-editor/src/components/block-drop-zone/style.scss +++ /dev/null @@ -1,27 +0,0 @@ -// Dropzones -.block-editor-block-drop-zone { - border: none; - border-radius: 0; - - .components-drop-zone__content, - &.is-dragging-over-element .components-drop-zone__content { - display: none; - } - - &.is-close-to-bottom, - &.is-close-to-top { - background: none; - } - - &.is-close-to-top { - border-top: 3px solid theme(primary); - } - - &.is-close-to-bottom { - border-bottom: 3px solid theme(primary); - } - - &.is-appender.is-active.is-dragging-over-document { - border-bottom: none; - } -} diff --git a/packages/block-editor/src/components/block-list-appender/index.js b/packages/block-editor/src/components/block-list-appender/index.js index 14b6579d8f5a4..6515eea231b4e 100644 --- a/packages/block-editor/src/components/block-list-appender/index.js +++ b/packages/block-editor/src/components/block-list-appender/index.js @@ -2,6 +2,7 @@ * External dependencies */ import { last } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -25,6 +26,7 @@ function BlockListAppender( { canInsertDefaultBlock, isLocked, renderAppender: CustomAppender, + className, } ) { if ( isLocked || CustomAppender === false ) { return null; @@ -68,7 +70,7 @@ function BlockListAppender( { // Prevent the block from being selected when the appender is // clicked. onFocus={ stopPropagation } - className="block-list-appender" + className={ classnames( 'block-list-appender', className ) } > { appender } 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 a219f66813448..3b05f1aac5599 100644 --- a/packages/block-editor/src/components/block-list-appender/style.scss +++ b/packages/block-editor/src/components/block-list-appender/style.scss @@ -10,6 +10,16 @@ } } +.block-list-appender.is-drop-target > div::before { + content: ""; + position: absolute; + right: -$block-padding; + left: -$block-padding; + top: -$block-padding; + bottom: -$block-padding; + border: 3px solid theme(primary); +} + .block-list-appender > .block-editor-inserter { display: block; } diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 7074d44e00f0e..fefebfc53d9fc 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -37,7 +37,6 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts'; * Internal dependencies */ import BlockEdit from '../block-edit'; -import BlockDropZone from '../block-drop-zone'; import BlockInvalidWarning from './block-invalid-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; @@ -284,14 +283,6 @@ function BlockListBlock( { isFirstMultiSelected ); - // Insertion point can only be made visible if the block is at the - // the extent of a multi-selection, or not in a multi-selection. - const shouldShowInsertionPoint = ! isMultiSelecting && ( - ( isPartOfMultiSelection && isFirstMultiSelected ) || - ! isPartOfMultiSelection - ); - - const shouldRenderDropzone = shouldShowInsertionPoint; const isDragging = isDraggingBlocks && ( isSelected || isPartOfMultiSelection ); // The wp-block className is important for editor styles. @@ -394,10 +385,6 @@ function BlockListBlock( { animationStyle } > - { shouldRenderDropzone && } { hasAncestorCapturingToolbars && ( shouldShowContextualToolbar || isToolbarForced ) && ( // If the parent Block is set to consume toolbars of the child Blocks // then render the child Block's toolbar into the Slot provided diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index dff5d1c980298..6c74079754781 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -7,6 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { AsyncModeProvider, useSelect } from '@wordpress/data'; +import { useRef } from '@wordpress/element'; /** * Internal dependencies @@ -15,6 +16,7 @@ import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; import __experimentalBlockListFooter from '../block-list-footer'; import RootContainer from './root-container'; +import useBlockDropZone from '../block-drop-zone'; /** * If the block count exceeds the threshold, we disable the reordering animation @@ -79,8 +81,16 @@ function BlockList( { const Container = rootClientId ? 'div' : RootContainer; + const ref = useRef(); + + const targetClientId = useBlockDropZone( { + element: ref, + rootClientId, + } ); + return ( ); @@ -113,6 +124,7 @@ function BlockList( { <__experimentalBlockListFooter.Slot /> diff --git a/packages/block-editor/src/components/block-list/root-container.js b/packages/block-editor/src/components/block-list/root-container.js index 287284aafd71f..437be8f1cd795 100644 --- a/packages/block-editor/src/components/block-list/root-container.js +++ b/packages/block-editor/src/components/block-list/root-container.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useRef, createContext } from '@wordpress/element'; +import { createContext, forwardRef } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; /** @@ -44,8 +44,7 @@ function onDragStart( event ) { } } -export default function RootContainer( { children, className } ) { - const ref = useRef(); +function RootContainer( { children, className }, ref ) { const { selectedBlockClientId, hasMultiSelection, @@ -92,3 +91,5 @@ export default function RootContainer( { children, className } ) { ); } + +export default forwardRef( RootContainer ); diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 1ccf86d9b6479..ef97f229c3e3e 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -152,6 +152,10 @@ opacity: 1; } } + + &.is-drop-target::before { + border-top: 3px solid theme(primary); + } } @@ -351,13 +355,6 @@ float: none; } - // Dropzones. - .block-editor-block-drop-zone { - top: -4px; - bottom: -3px; - margin: 0 $block-padding; - } - // This essentially duplicates the mobile styles for the appender component. // It would be nice to be able to use element queries in that component instead https://github.com/tomhodgins/element-queries-spec .block-editor-block-list__layout { diff --git a/packages/block-editor/src/components/button-block-appender/index.js b/packages/block-editor/src/components/button-block-appender/index.js index e3e81f61cb0f5..04ca29a71079f 100644 --- a/packages/block-editor/src/components/button-block-appender/index.js +++ b/packages/block-editor/src/components/button-block-appender/index.js @@ -12,44 +12,40 @@ import { _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import BlockDropZone from '../block-drop-zone'; import Inserter from '../inserter'; function ButtonBlockAppender( { rootClientId, className, __experimentalSelectBlockOnInsert: selectBlockOnInsert } ) { return ( - <> - - { - let label; - if ( hasSingleBlockType ) { - // translators: %s: the name of the block when there is only one - label = sprintf( _x( 'Add %s', 'directly add the only allowed block' ), blockTitle ); - } else { - label = _x( 'Add block', 'Generic label for block inserter button' ); - } - const isToggleButton = ! hasSingleBlockType; - return ( - - - - ); - } } - isAppender - /> - + { + let label; + if ( hasSingleBlockType ) { + // translators: %s: the name of the block when there is only one + label = sprintf( _x( 'Add %s', 'directly add the only allowed block' ), blockTitle ); + } else { + label = _x( 'Add block', 'Generic label for block inserter button' ); + } + const isToggleButton = ! hasSingleBlockType; + return ( + + + + ); + } } + isAppender + /> ); } diff --git a/packages/block-editor/src/components/default-block-appender/index.js b/packages/block-editor/src/components/default-block-appender/index.js index ec07093b849a9..29262787f095d 100644 --- a/packages/block-editor/src/components/default-block-appender/index.js +++ b/packages/block-editor/src/components/default-block-appender/index.js @@ -15,7 +15,6 @@ import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import BlockDropZone from '../block-drop-zone'; import Inserter from '../inserter'; export function DefaultBlockAppender( { @@ -52,7 +51,6 @@ export function DefaultBlockAppender( { data-root-client-id={ rootClientId || '' } className="wp-block block-editor-default-block-appender" > - ); } + export default compose( withSelect( ( select, ownProps ) => { const { getBlockCount, getBlockName, isBlockValid, getSettings, getTemplateLock } = select( 'core/block-editor' ); diff --git a/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap index d9a8e74b5994a..b237704948105 100644 --- a/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/default-block-appender/test/__snapshots__/index.js.snap @@ -5,7 +5,6 @@ exports[`DefaultBlockAppender should append a default block when input focused 1 className="wp-block block-editor-default-block-appender" data-root-client-id="" > - - - { + if ( ! isDisabled ) { + const dropZone = { + element, + onDrop, + onFilesDrop, + onHTMLDrop, + setState, + withPosition, + }; + addDropZone( dropZone ); + return () => { + removeDropZone( dropZone ); + }; + } + }, [ isDisabled, onDrop, onFilesDrop, onHTMLDrop, withPosition ] ); + + return state; +} const DropZone = ( props ) => ( @@ -27,76 +62,53 @@ const DropZone = ( props ) => ( ); -class DropZoneComponent extends Component { - constructor() { - super( ...arguments ); - - this.dropZoneElement = createRef(); - this.dropZone = { - element: null, - onDrop: this.props.onDrop, - onFilesDrop: this.props.onFilesDrop, - onHTMLDrop: this.props.onHTMLDrop, - setState: this.setState.bind( this ), - }; - this.state = { - isDraggingOverDocument: false, - isDraggingOverElement: false, - position: null, - type: null, - }; - } +function DropZoneComponent( { + className, + label, + onFilesDrop, + onHTMLDrop, + onDrop, +} ) { + const element = useRef(); + const { + isDraggingOverDocument, + isDraggingOverElement, + type, + } = useDropZone( { element, onFilesDrop, onHTMLDrop, onDrop } ); - componentDidMount() { - // Set element after the component has a node assigned in the DOM - this.dropZone.element = this.dropZoneElement.current; - this.props.addDropZone( this.dropZone ); - } + let children; - componentWillUnmount() { - this.props.removeDropZone( this.dropZone ); - } - - render() { - const { className, label, onFilesDrop, onHTMLDrop, onDrop } = this.props; - const { isDraggingOverDocument, isDraggingOverElement, position, type } = this.state; - const classes = classnames( 'components-drop-zone', className, { - 'is-active': ( isDraggingOverDocument || isDraggingOverElement ) && ( - ( type === 'file' && onFilesDrop ) || - ( type === 'html' && onHTMLDrop ) || - ( type === 'default' && onDrop ) - ), - 'is-dragging-over-document': isDraggingOverDocument, - 'is-dragging-over-element': isDraggingOverElement, - 'is-close-to-top': position && position.y === 'top', - 'is-close-to-bottom': position && position.y === 'bottom', - 'is-close-to-left': position && position.x === 'left', - 'is-close-to-right': position && position.x === 'right', - [ `is-dragging-${ type }` ]: !! type, - } ); - - let children; - if ( isDraggingOverElement ) { - children = ( -
- - - { label ? label : __( 'Drop files to upload' ) } - -
- ); - } - - return ( -
- { children } + if ( isDraggingOverElement ) { + children = ( +
+ + + { label ? label : __( 'Drop files to upload' ) } +
); } + + const classes = classnames( 'components-drop-zone', className, { + 'is-active': ( isDraggingOverDocument || isDraggingOverElement ) && ( + ( type === 'file' && onFilesDrop ) || + ( type === 'html' && onHTMLDrop ) || + ( type === 'default' && onDrop ) + ), + 'is-dragging-over-document': isDraggingOverDocument, + 'is-dragging-over-element': isDraggingOverElement, + [ `is-dragging-${ type }` ]: !! type, + } ); + + return ( +
+ { children } +
+ ); } export default DropZone; diff --git a/packages/components/src/drop-zone/provider.js b/packages/components/src/drop-zone/provider.js index a0df1364b8ac7..45c0a70749c38 100644 --- a/packages/components/src/drop-zone/provider.js +++ b/packages/components/src/drop-zone/provider.js @@ -9,11 +9,13 @@ import { isEqual, find, some, filter, throttle, includes } from 'lodash'; import { Component, createContext } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; -const { Provider, Consumer } = createContext( { +export const Context = createContext( { addDropZone: () => {}, removeDropZone: () => {}, } ); +const { Provider, Consumer } = Context; + const getDragEventType = ( { dataTransfer } ) => { if ( dataTransfer ) { // Use lodash `includes` here as in the Edge browser `types` is implemented @@ -126,25 +128,20 @@ class DropZoneProvider extends Component { // Index of hovered dropzone. const hoveredDropZones = filter( this.dropZones, ( dropZone ) => isTypeSupportedByDropZone( dragEventType, dropZone ) && - isWithinElementBounds( dropZone.element, detail.clientX, detail.clientY ) + isWithinElementBounds( dropZone.element.current, detail.clientX, detail.clientY ) ); // Find the leaf dropzone not containing another dropzone const hoveredDropZone = find( hoveredDropZones, ( zone ) => ( - ! some( hoveredDropZones, ( subZone ) => subZone !== zone && zone.element.parentElement.contains( subZone.element ) ) + ! some( hoveredDropZones, ( subZone ) => subZone !== zone && zone.element.current.parentElement.contains( subZone.element.current ) ) ) ); const hoveredDropZoneIndex = this.dropZones.indexOf( hoveredDropZone ); let position = null; - if ( hoveredDropZone ) { - const rect = hoveredDropZone.element.getBoundingClientRect(); - - position = { - x: detail.clientX - rect.left < rect.right - detail.clientX ? 'left' : 'right', - y: detail.clientY - rect.top < rect.bottom - detail.clientY ? 'top' : 'bottom', - }; + if ( hoveredDropZone && hoveredDropZone.withPosition ) { + position = { x: detail.clientX, y: detail.clientY }; } // Optimisation: Only update the changed dropzones diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 8fb840d437200..d59d33984f16c 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -22,7 +22,7 @@ export { DateTimePicker, DatePicker, TimePicker } from './date-time'; export { default as __experimentalDimensionControl } from './dimension-control'; export { default as Disabled } from './disabled'; export { default as Draggable } from './draggable'; -export { default as DropZone } from './drop-zone'; +export { default as DropZone, useDropZone as __unstableUseDropZone } from './drop-zone'; export { default as DropZoneProvider } from './drop-zone/provider'; export { default as Dropdown } from './dropdown'; export { default as DropdownMenu } from './dropdown-menu';