From 6e8369993e184b41090907fcc4e9fcfd141af6e1 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Mon, 3 Oct 2022 23:46:30 +0800 Subject: [PATCH 1/8] Try integrating block-popover-drop-zone with insertion-point --- .../src/components/block-popover/drop-zone.js | 63 +++++ .../src/components/block-popover/style.scss | 18 +- .../components/block-tools/insertion-point.js | 21 +- packages/block-editor/src/components/index.js | 1 - .../list-view/use-list-view-drop-zone.js | 22 +- .../components/use-block-drop-zone/index.js | 163 ++++++++---- .../use-block-drop-zone/test/index.js | 232 +++++++++++------- .../src/components/use-on-block-drop/index.js | 23 +- .../src/components/use-on-block-drop/types.ts | 1 + packages/block-editor/src/store/actions.js | 3 +- packages/block-editor/src/store/reducer.js | 16 +- packages/block-editor/src/utils/math.js | 17 ++ .../block-library/src/paragraph/drop-zone.js | 105 -------- packages/block-library/src/paragraph/edit.js | 15 +- .../block-library/src/paragraph/editor.scss | 20 -- packages/components/src/index.js | 2 +- .../e2e-test-utils-playwright/src/test.ts | 5 + .../e2e/specs/editor/blocks/paragraph.spec.js | 6 +- 18 files changed, 408 insertions(+), 325 deletions(-) create mode 100644 packages/block-editor/src/components/block-popover/drop-zone.js create mode 100644 packages/block-editor/src/components/use-on-block-drop/types.ts delete mode 100644 packages/block-library/src/paragraph/drop-zone.js diff --git a/packages/block-editor/src/components/block-popover/drop-zone.js b/packages/block-editor/src/components/block-popover/drop-zone.js new file mode 100644 index 0000000000000..6ef3621bd9edb --- /dev/null +++ b/packages/block-editor/src/components/block-popover/drop-zone.js @@ -0,0 +1,63 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useReducedMotion } from '@wordpress/compose'; +import { __unstableMotion as motion } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import BlockPopover from './index'; + +const animateVariants = { + hide: { opacity: 0, scaleY: 0.75 }, + show: { opacity: 1, scaleY: 1 }, + exit: { opacity: 0, scaleY: 0.9 }, +}; + +function BlockPopoverDropZone( { + __unstablePopoverSlot, + __unstableContentRef, +} ) { + const { clientId } = useSelect( ( select ) => { + const { getBlockOrder, getBlockInsertionPoint } = + select( blockEditorStore ); + const insertionPoint = getBlockInsertionPoint(); + const order = getBlockOrder( insertionPoint.rootClientId ); + + if ( ! order.length ) { + return {}; + } + + return { + clientId: order[ insertionPoint.index ], + }; + }, [] ); + const reducedMotion = useReducedMotion(); + + return ( + + + + ); +} + +export default BlockPopoverDropZone; diff --git a/packages/block-editor/src/components/block-popover/style.scss b/packages/block-editor/src/components/block-popover/style.scss index 1ed4774a56c01..1744506179e67 100644 --- a/packages/block-editor/src/components/block-popover/style.scss +++ b/packages/block-editor/src/components/block-popover/style.scss @@ -21,7 +21,7 @@ } // Enable pointer events for the toolbar's content. - &:not(.block-editor-block-popover__inbetween) .components-popover__content { + &:not(.block-editor-block-popover__inbetween, .block-editor-block-popover__drop-zone) .components-popover__content { * { pointer-events: all; } @@ -48,3 +48,19 @@ } } } + + +.components-popover.block-editor-block-popover__drop-zone { + // Disable pointer events for dragging and dropping. + // This drop zone is fully presentational, the actual DnD implementation is handled elsewhere. + * { + pointer-events: none; + } + + .block-editor-block-popover__drop-zone-foreground { + position: absolute; + inset: 0; + background-color: var(--wp-admin-theme-color); + border-radius: 2px; + } +} diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 4f98a29a6e0ef..2c5d6a8563087 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -17,10 +17,11 @@ import { useReducedMotion } from '@wordpress/compose'; import Inserter from '../inserter'; import { store as blockEditorStore } from '../../store'; import BlockPopoverInbetween from '../block-popover/inbetween'; +import BlockPopoverDropZone from '../block-popover/drop-zone'; export const InsertionPointOpenRef = createContext(); -function InsertionPointPopover( { +function InBetweenInsertionPointPopover( { __unstablePopoverSlot, __unstableContentRef, } ) { @@ -232,9 +233,21 @@ function InsertionPointPopover( { } export default function InsertionPoint( props ) { - const isVisible = useSelect( ( select ) => { - return select( blockEditorStore ).isBlockInsertionPointVisible(); + const { insertionPoint, isVisible } = useSelect( ( select ) => { + const { getBlockInsertionPoint, isBlockInsertionPointVisible } = + select( blockEditorStore ); + return { + insertionPoint: getBlockInsertionPoint(), + isVisible: isBlockInsertionPointVisible(), + }; }, [] ); - return isVisible && ; + return ( + isVisible && + ( insertionPoint.operation === 'replace' ? ( + + ) : ( + + ) ) + ); } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 0ad7de536282e..6b7a24887b342 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -155,7 +155,6 @@ export { export { default as __experimentalBlockPatternsList } from './block-patterns-list'; export { default as __experimentalPublishDateTimePicker } from './publish-date-time-picker'; export { default as __experimentalInspectorPopoverHeader } from './inspector-popover-header'; -export { default as __experimentalUseOnBlockDrop } from './use-on-block-drop'; /* * State Related Components diff --git a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js index 731ae2530ec11..346631667c254 100644 --- a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js +++ b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js @@ -11,7 +11,10 @@ import { /** * Internal dependencies */ -import { getDistanceToNearestEdge } from '../../utils/math'; +import { + getDistanceToNearestEdge, + isPointContainedByRect, +} from '../../utils/math'; import useOnBlockDrop from '../use-on-block-drop'; import { store as blockEditorStore } from '../../store'; @@ -48,23 +51,6 @@ import { store as blockEditorStore } from '../../store'; * 'inside' refers to nesting as an inner block. */ -/** - * Is the point contained by the rectangle. - * - * @param {WPPoint} point The point. - * @param {DOMRect} rect The rectangle. - * - * @return {boolean} True if the point is contained by the rectangle, false otherwise. - */ -function isPointContainedByRect( point, rect ) { - return ( - rect.left <= point.x && - rect.right >= point.x && - rect.top <= point.y && - rect.bottom >= point.y - ); -} - /** * Determines whether the user positioning the dragged block to nest as an * inner block. 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 3bef31546c711..1786c39d8740b 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,15 +8,20 @@ import { __experimentalUseDropZone as useDropZone, } from '@wordpress/compose'; import { isRTL } from '@wordpress/i18n'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies */ import useOnBlockDrop from '../use-on-block-drop'; -import { getDistanceToNearestEdge } from '../../utils/math'; +import { + getDistanceToNearestEdge, + isPointContainedByRect, +} from '../../utils/math'; import { store as blockEditorStore } from '../../store'; /** @typedef {import('../../utils/math').WPPoint} WPPoint */ +/** @typedef {import('../use-on-block-drop/types').WPDropOperation} WPDropOperation */ /** * The orientation of a block list. @@ -24,6 +29,12 @@ import { store as blockEditorStore } from '../../store'; * @typedef {'horizontal'|'vertical'|undefined} WPBlockListOrientation */ +/** + * The insert position when dropping a block. + * + * @typedef {'before'|'after'} WPInsertPosition + */ + /** * Given a list of block DOM elements finds the index that a block should be dropped * at. @@ -32,7 +43,7 @@ import { store as blockEditorStore } from '../../store'; * @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. + * @return {[number|undefined, WPInsertPosition]} The block index and the position that's closest to the drag position. */ export function getNearestBlockIndex( elements, position, orientation ) { const allowedEdges = @@ -43,48 +54,79 @@ export function getNearestBlockIndex( elements, position, orientation ) { const isRightToLeft = isRTL(); let candidateIndex; + let candidatePosition = 'after'; let candidateDistance; elements.forEach( ( element, index ) => { const rect = element.getBoundingClientRect(); - const [ distance, edge ] = getDistanceToNearestEdge( + + let [ distance, edge ] = getDistanceToNearestEdge( position, rect, allowedEdges ); + // Prioritize the element if the point is inside of it. + if ( isPointContainedByRect( position, rect ) ) { + distance = 0; + } 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. - // Take RTL languages into account where the left edge is - // the trailing edge. - const isTrailingEdge = + // Where the dropped block will be inserted on the nearest block. + candidatePosition = edge === 'bottom' || ( ! isRightToLeft && edge === 'right' ) || - ( isRightToLeft && edge === 'left' ); - const offset = isTrailingEdge ? 1 : 0; + ( isRightToLeft && edge === 'left' ) + ? 'after' + : 'before'; // Update the currently known best candidate. candidateDistance = distance; - candidateIndex = index + offset; + candidateIndex = index; } } ); - return candidateIndex; + return [ candidateIndex, candidatePosition ]; } /** - * Determine if the element is an empty paragraph block. + * Get the drop target index and operation based on the the blocks and the nearst block index. * - * @param {?HTMLElement} element The element being tested. - * @return {boolean} True or False. + * @param {number|undefined} nearestIndex The nearest block index calculated by getNearestBlockIndex. + * @param {WPInsertPosition} insertPosition Whether to insert before or after the nearestIndex. + * @param {WPBlock[]} blocks The blocks list. + * @return {[number, WPDropOperation]} The drop target. */ -function isEmptyParagraph( element ) { - return ( - !! element && - element.dataset.type === 'core/paragraph' && - element.dataset.empty === 'true' - ); +export function getDropTargetIndexAndOperation( + nearestIndex, + insertPosition, + blocks +) { + const adjacentIndex = + nearestIndex + ( insertPosition === 'after' ? 1 : -1 ); + const nearestBlock = blocks[ nearestIndex ]; + const adjacentBlock = blocks[ adjacentIndex ]; + const isNearestBlockUnmodifiedDefaultBlock = + !! nearestBlock && isUnmodifiedDefaultBlock( nearestBlock ); + const isAdjacentBlockUnmodifiedDefaultBlock = + !! adjacentBlock && isUnmodifiedDefaultBlock( adjacentBlock ); + + // If both blocks are not unmodified default blocks then just insert between them. + if ( + ! isNearestBlockUnmodifiedDefaultBlock && + ! isAdjacentBlockUnmodifiedDefaultBlock + ) { + // If the user is dropping to the trailing edge of the block + // add 1 to the index to represent dragging after. + const insertionIndex = + insertPosition === 'after' ? nearestIndex + 1 : nearestIndex; + return [ insertionIndex, 'insert' ]; + } + + // Otherwise, replace the nearest unmodified default block. + return [ + isNearestBlockUnmodifiedDefaultBlock ? nearestIndex : adjacentIndex, + 'replace', + ]; } /** @@ -104,7 +146,10 @@ export default function useBlockDropZone( { // an empty string to represent top-level blocks. rootClientId: targetRootClientId = '', } = {} ) { - const [ targetBlockIndex, setTargetBlockIndex ] = useState( null ); + const [ dropTarget, setDropTarget ] = useState( { + index: null, + operation: 'insert', + } ); const isDisabled = useSelect( ( select ) => { @@ -125,40 +170,58 @@ export default function useBlockDropZone( { [ targetRootClientId ] ); - const { getBlockListSettings } = useSelect( blockEditorStore ); + const { getBlockListSettings, getBlocks } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); - const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex ); + const onBlockDrop = useOnBlockDrop( targetRootClientId, dropTarget.index, { + operation: dropTarget.operation, + } ); const throttled = useThrottle( - useCallback( ( event, currentTarget ) => { - const blockElements = Array.from( currentTarget.children ).filter( - // Ensure the element is a block. It should have the `wp-block` class. - ( element ) => element.classList.contains( 'wp-block' ) - ); - const targetIndex = getNearestBlockIndex( - blockElements, - { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation - ); - - setTargetBlockIndex( targetIndex === undefined ? 0 : targetIndex ); - - if ( targetIndex !== undefined ) { - const nextBlock = blockElements[ targetIndex ]; - const previousBlock = blockElements[ targetIndex - 1 ]; - - // Don't show the insertion point when it's near an empty paragraph block. - if ( - isEmptyParagraph( nextBlock ) || - isEmptyParagraph( previousBlock ) - ) { + useCallback( + ( event, currentTarget ) => { + const blockElements = Array.from( + currentTarget.children + ).filter( + // Ensure the element is a block. It should have the `wp-block` class. + ( element ) => element.classList.contains( 'wp-block' ) + ); + // The second value in the tuple is only needed afterwards but we don't want to recalculate it. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const [ nearestBlockIndex, insertPosition ] = + getNearestBlockIndex( + blockElements, + { x: event.clientX, y: event.clientY }, + getBlockListSettings( targetRootClientId )?.orientation + ); + + // The block list is empty, don't show the insertion point but still allow dropping. + if ( nearestBlockIndex === undefined ) { + setDropTarget( { + index: 0, + operation: 'insert', + } ); return; } - showInsertionPoint( targetRootClientId, targetIndex ); - } - }, [] ), + const blocks = getBlocks( targetRootClientId ); + const [ targetIndex, operation ] = + getDropTargetIndexAndOperation( + nearestBlockIndex, + insertPosition, + blocks + ); + + setDropTarget( { + index: targetIndex, + operation, + } ); + showInsertionPoint( targetRootClientId, targetIndex, { + operation, + } ); + }, + [ targetRootClientId ] + ), 200 ); @@ -174,12 +237,10 @@ export default function useBlockDropZone( { onDragLeave() { throttled.cancel(); hideInsertionPoint(); - setTargetBlockIndex( null ); }, onDragEnd() { throttled.cancel(); hideInsertionPoint(); - setTargetBlockIndex( null ); }, } ); } 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 index 492f89ff7467a..2cda7a805023c 100644 --- 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 @@ -1,7 +1,18 @@ +/** + * WordPress dependencies + */ +import { + registerBlockType, + unregisterBlockType, + createBlock, + getDefaultBlockName, + setDefaultBlockName, +} from '@wordpress/blocks'; + /** * Internal dependencies */ -import { getNearestBlockIndex } from '..'; +import { getNearestBlockIndex, getDropTargetIndexAndOperation } from '..'; const elementData = [ { @@ -79,13 +90,13 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBeUndefined(); + expect( result ).toEqual( [ undefined, 'after' ] ); } ); describe( 'Vertical block lists', () => { const orientation = 'vertical'; - it( 'returns `0` when the position is nearest to the start of the first block', () => { + it( "returns [0, 'before'] when the position is nearest to the start of the first block", () => { const position = { x: 0, y: 0 }; const result = getNearestBlockIndex( @@ -94,10 +105,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 0 ); + expect( result ).toEqual( [ 0, 'before' ] ); } ); - it( 'returns `1` when the position is nearest to the end of the first block', () => { + it( "returns [0, 'after'] when the position is nearest to the end of the first block", () => { const position = { x: 0, y: 190 }; const result = getNearestBlockIndex( @@ -106,10 +117,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 1 ); + expect( result ).toEqual( [ 0, 'after' ] ); } ); - it( 'returns `1` when the position is nearest to the start of the second block', () => { + it( "returns [1, 'before'] when the position is nearest to the start of the second block", () => { const position = { x: 0, y: 210 }; const result = getNearestBlockIndex( @@ -118,10 +129,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 1 ); + expect( result ).toEqual( [ 1, 'before' ] ); } ); - it( 'returns `2` when the position is nearest to the end of the second block', () => { + it( "returns [1, 'after'] when the position is nearest to the end of the second block", () => { const position = { x: 0, y: 450 }; const result = getNearestBlockIndex( @@ -130,46 +141,10 @@ describe( 'getNearestBlockIndex', () => { 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 ); + expect( result ).toEqual( [ 1, 'after' ] ); } ); - 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', () => { + it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { const position = { x: 401, y: 0 }; const result = getNearestBlockIndex( @@ -178,10 +153,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 3 ); + expect( result ).toEqual( [ 3, 'before' ] ); } ); - it( 'returns `4` when the position is nearest to the end of the fourth block', () => { + it( "returns [3, 'after'] when the position is nearest to the end of the last block", () => { const position = { x: 401, y: 300 }; const result = getNearestBlockIndex( @@ -190,14 +165,14 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 4 ); + expect( result ).toEqual( [ 3, 'after' ] ); } ); } ); describe( 'Horizontal block lists', () => { const orientation = 'horizontal'; - it( 'returns `0` when the position is nearest to the start of the first block', () => { + it( "returns [0, 'before'] when the position is nearest to the start of the first block", () => { const position = { x: 0, y: 0 }; const result = getNearestBlockIndex( @@ -206,10 +181,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 0 ); + expect( result ).toEqual( [ 0, 'before' ] ); } ); - it( 'returns `1` when the position is nearest to the end of the first block', () => { + it( "returns [0, 'after'] when the position is nearest to the end of the first block", () => { const position = { x: 190, y: 0 }; const result = getNearestBlockIndex( @@ -218,10 +193,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 1 ); + expect( result ).toEqual( [ 0, 'after' ] ); } ); - it( 'returns `1` when the position is nearest to the start of the second block', () => { + it( "returns [1, 'before'] when the position is nearest to the start of the second block", () => { const position = { x: 210, y: 0 }; const result = getNearestBlockIndex( @@ -230,10 +205,10 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 1 ); + expect( result ).toEqual( [ 1, 'before' ] ); } ); - it( 'returns `2` when the position is nearest to the end of the second block', () => { + it( "returns [1, 'after'] when the position is nearest to the end of the second block", () => { const position = { x: 450, y: 0 }; const result = getNearestBlockIndex( @@ -242,11 +217,11 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 2 ); + expect( result ).toEqual( [ 1, 'after' ] ); } ); - it( 'returns `2` when the position is nearest to the start of the third block', () => { - const position = { x: 510, y: 0 }; + it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { + const position = { x: 0, y: 401 }; const result = getNearestBlockIndex( horizontalElements, @@ -254,11 +229,11 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 2 ); + expect( result ).toEqual( [ 3, 'before' ] ); } ); - it( 'returns `3` when the position is nearest to the end of the third block', () => { - const position = { x: 880, y: 0 }; + it( "returns [3, 'after'] when the position is nearest to the end of the fourth block", () => { + const position = { x: 300, y: 401 }; const result = getNearestBlockIndex( horizontalElements, @@ -266,43 +241,118 @@ describe( 'getNearestBlockIndex', () => { orientation ); - expect( result ).toBe( 3 ); + expect( result ).toEqual( [ 3, 'after' ] ); } ); + } ); +} ); - 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 - ); +describe( 'getDropTargetIndexAndOperation', () => { + let defaultBlockName; - expect( result ).toBe( 3 ); + beforeAll( () => { + defaultBlockName = getDefaultBlockName(); + registerBlockType( 'test/default-block', { title: 'default block' } ); + registerBlockType( 'test/not-default-block', { + title: 'not default block', } ); + setDefaultBlockName( 'test/default-block' ); + } ); - it( 'returns `3` when the position is nearest to the start of the fourth block', () => { - const position = { x: 0, y: 401 }; + afterAll( () => { + setDefaultBlockName( defaultBlockName ); + unregisterBlockType( 'test/default-block' ); + unregisterBlockType( 'test/not-default-block' ); + } ); - const result = getNearestBlockIndex( - horizontalElements, - position, - orientation - ); + it( 'returns insertion index when there are no unmodified default blocks', () => { + const blocks = [ + createBlock( 'test/not-default-block' ), + createBlock( 'test/not-default-block' ), + ]; - expect( result ).toBe( 3 ); - } ); + expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( + [ 0, 'insert' ] + ); - it( 'returns `4` when the position is nearest to the end of the fourth block', () => { - const position = { x: 300, y: 401 }; + expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( + [ 1, 'insert' ] + ); - const result = getNearestBlockIndex( - horizontalElements, - position, - orientation - ); + expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( + [ 1, 'insert' ] + ); - expect( result ).toBe( 4 ); - } ); + expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( + [ 2, 'insert' ] + ); + } ); + + it( 'handles replacement index when only the first block is an unmodified default block', () => { + const blocks = [ + createBlock( 'test/default-block' ), + createBlock( 'test/not-default-block' ), + ]; + + expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( + [ 0, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( + [ 0, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( + [ 0, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( + [ 2, 'insert' ] + ); + } ); + + it( 'handles replacement index when only the second block is an unmodified default block', () => { + const blocks = [ + createBlock( 'test/not-default-block' ), + createBlock( 'test/default-block' ), + ]; + + expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( + [ 0, 'insert' ] + ); + + expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( + [ 1, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( + [ 1, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( + [ 1, 'replace' ] + ); + } ); + + it( 'returns replacement index when both blocks are unmodified default blocks', () => { + const blocks = [ + createBlock( 'test/default-block' ), + createBlock( 'test/default-block' ), + ]; + + expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( + [ 0, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( + [ 0, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( + [ 1, 'replace' ] + ); + + expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( + [ 1, 'replace' ] + ); } ); } ); diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 245f912bcf4da..6b0a930012449 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -17,6 +17,7 @@ import { getFilesFromDataTransfer } from '@wordpress/dom'; import { store as blockEditorStore } from '../../store'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ +/** @typedef {import('./types').WPDropOperation} WPDropOperation */ /** * Retrieve the data for a block drop event. @@ -197,21 +198,19 @@ export function onHTMLDrop( /** * A React hook for handling block drop events. * - * @typedef {'insert'|'replace'} DropAction The type of action to perform on drop. + * @param {string} targetRootClientId The root client id where the block(s) will be inserted. + * @param {number} targetBlockIndex The index where the block(s) will be inserted. + * @param {Object} options The optional options. + * @param {WPDropOperation} [options.operation] The type of operation to perform on drop. Could be `insert` or `replace` for now. * - * @param {string} targetRootClientId The root client id where the block(s) will be inserted. - * @param {number} targetBlockIndex The index where the block(s) will be inserted. - * @param {Object} options The optional options. - * @param {DropAction} options.action The type of action to perform on drop. Could be `insert` or `replace` for now. - * - * @return {Object} An object that contains the event handlers `onDrop`, `onFilesDrop` and `onHTMLDrop`. + * @return {Function} A function to be passed to the onDrop handler. */ export default function useOnBlockDrop( targetRootClientId, targetBlockIndex, options = {} ) { - const { action = 'insert' } = options; + const { operation = 'insert' } = options; const hasUploadPermissions = useSelect( ( select ) => select( blockEditorStore ).getSettings().mediaUpload, [] @@ -235,7 +234,7 @@ export default function useOnBlockDrop( const insertOrReplaceBlocks = useCallback( ( blocks, updateSelection = true, initialPosition = 0 ) => { - if ( action === 'replace' ) { + if ( operation === 'replace' ) { const clientIds = getBlockOrder( targetRootClientId ); const clientId = clientIds[ targetBlockIndex ]; @@ -251,7 +250,7 @@ export default function useOnBlockDrop( } }, [ - action, + operation, getBlockOrder, insertBlocks, replaceBlocks, @@ -262,7 +261,7 @@ export default function useOnBlockDrop( const moveBlocks = useCallback( ( sourceClientIds, sourceRootClientId, insertIndex ) => { - if ( action === 'replace' ) { + if ( operation === 'replace' ) { const sourceBlocks = getBlocksByClientId( sourceClientIds ); const targetBlockClientIds = getBlockOrder( targetRootClientId ); @@ -290,7 +289,7 @@ export default function useOnBlockDrop( } }, [ - action, + operation, getBlockOrder, getBlocksByClientId, insertBlocks, diff --git a/packages/block-editor/src/components/use-on-block-drop/types.ts b/packages/block-editor/src/components/use-on-block-drop/types.ts new file mode 100644 index 0000000000000..1b24ea7e90f9a --- /dev/null +++ b/packages/block-editor/src/components/use-on-block-drop/types.ts @@ -0,0 +1 @@ +export type WPDropOperation = 'insert' | 'replace'; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 68b6f61ca89d6..3b9bf9c9b9f9f 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -636,12 +636,13 @@ export function showInsertionPoint( index, __unstableOptions = {} ) { - const { __unstableWithInserter } = __unstableOptions; + const { __unstableWithInserter, operation } = __unstableOptions; return { type: 'SHOW_INSERTION_POINT', rootClientId, index, __unstableWithInserter, + operation, }; } /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index e306829c22ed5..3c04134f8ffd8 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1475,9 +1475,19 @@ export function blocksMode( state = {}, action ) { */ export function insertionPoint( state = null, action ) { switch ( action.type ) { - case 'SHOW_INSERTION_POINT': - const { rootClientId, index, __unstableWithInserter } = action; - return { rootClientId, index, __unstableWithInserter }; + case 'SHOW_INSERTION_POINT': { + const { rootClientId, index, __unstableWithInserter, operation } = + action; + const nextState = { + rootClientId, + index, + __unstableWithInserter, + operation, + }; + + // Bail out updates if the states are the same. + return isEqual( state, nextState ) ? state : nextState; + } case 'HIDE_INSERTION_POINT': return null; diff --git a/packages/block-editor/src/utils/math.js b/packages/block-editor/src/utils/math.js index f5140f446c6a1..128972e8a400e 100644 --- a/packages/block-editor/src/utils/math.js +++ b/packages/block-editor/src/utils/math.js @@ -89,3 +89,20 @@ export function getDistanceToNearestEdge( return [ candidateDistance, candidateEdge ]; } + +/** + * Is the point contained by the rectangle. + * + * @param {WPPoint} point The point. + * @param {DOMRect} rect The rectangle. + * + * @return {boolean} True if the point is contained by the rectangle, false otherwise. + */ +export function isPointContainedByRect( point, rect ) { + return ( + rect.left <= point.x && + rect.right >= point.x && + rect.top <= point.y && + rect.bottom >= point.y + ); +} diff --git a/packages/block-library/src/paragraph/drop-zone.js b/packages/block-library/src/paragraph/drop-zone.js deleted file mode 100644 index e51fb84acf806..0000000000000 --- a/packages/block-library/src/paragraph/drop-zone.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; -import { - __experimentalUseOnBlockDrop as useOnBlockDrop, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { - __experimentalUseDropZone as useDropZone, - useReducedMotion, -} from '@wordpress/compose'; -import { - Popover, - __unstableMotion as motion, - __unstableAnimatePresence as AnimatePresence, -} from '@wordpress/components'; - -const animateVariants = { - hide: { opacity: 0, scaleY: 0.75 }, - show: { opacity: 1, scaleY: 1 }, - exit: { opacity: 0, scaleY: 0.9 }, -}; - -export default function DropZone( { paragraphElement, clientId } ) { - const { rootClientId, blockIndex } = useSelect( - ( select ) => { - const selectors = select( blockEditorStore ); - return { - rootClientId: selectors.getBlockRootClientId( clientId ), - blockIndex: selectors.getBlockIndex( clientId ), - }; - }, - [ clientId ] - ); - const onBlockDrop = useOnBlockDrop( rootClientId, blockIndex, { - action: 'replace', - } ); - const [ isDragging, setIsDragging ] = useState( false ); - const [ isVisible, setIsVisible ] = useState( false ); - const popoverRef = useDropZone( { - onDragStart: () => { - setIsDragging( true ); - }, - onDragEnd: () => { - setIsDragging( false ); - }, - } ); - const dropZoneRef = useDropZone( { - onDrop: onBlockDrop, - onDragEnter: () => { - setIsVisible( true ); - }, - onDragLeave: () => { - setIsVisible( false ); - }, - } ); - const reducedMotion = useReducedMotion(); - - return ( - - { isDragging ? ( -
- - { isVisible ? ( - - ) : null } - -
- ) : null } -
- ); -} diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index c049b0b72bd24..5340eb5e4ea54 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -6,7 +6,6 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; import { __, _x, isRTL } from '@wordpress/i18n'; import { ToolbarButton, @@ -21,7 +20,6 @@ import { useBlockProps, useSetting, } from '@wordpress/block-editor'; -import { useMergeRefs } from '@wordpress/compose'; import { createBlock } from '@wordpress/blocks'; import { formatLtr } from '@wordpress/icons'; @@ -29,7 +27,6 @@ import { formatLtr } from '@wordpress/icons'; * Internal dependencies */ import { useOnEnter } from './use-enter'; -import DropZone from './drop-zone'; const name = 'core/paragraph'; @@ -62,12 +59,8 @@ function ParagraphBlock( { } ) { const { align, content, direction, dropCap, placeholder } = attributes; const isDropCapFeatureEnabled = useSetting( 'typography.dropCap' ); - const [ paragraphElement, setParagraphElement ] = useState( null ); const blockProps = useBlockProps( { - ref: useMergeRefs( [ - useOnEnter( { clientId, content } ), - setParagraphElement, - ] ), + ref: useOnEnter( { clientId, content } ), className: classnames( { 'has-drop-cap': hasDropCapDisabled( align ) ? false : dropCap, [ `has-text-align-${ align }` ]: align, @@ -130,12 +123,6 @@ function ParagraphBlock( { ) } - { ! content && ( - - ) } { await dragOver( '[data-type="core/paragraph"]' ); await expect( - page.locator( 'data-testid=empty-paragraph-drop-zone' ) + page.locator( 'data-testid=block-popover-drop-zone' ) ).toBeVisible(); await drop(); @@ -118,7 +118,7 @@ test.describe( 'Paragraph', () => { } await expect( - page.locator( 'data-testid=empty-paragraph-drop-zone' ) + page.locator( 'data-testid=block-popover-drop-zone' ) ).toBeVisible(); await page.mouse.up(); @@ -178,7 +178,7 @@ test.describe( 'Paragraph', () => { } await expect( - page.locator( 'data-testid=empty-paragraph-drop-zone' ) + page.locator( 'data-testid=block-popover-drop-zone' ) ).toBeVisible(); await page.mouse.up(); From bf4ccb12d6ecdf15a855ff69a6fc90376fb51e31 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 4 Oct 2022 09:39:41 +0800 Subject: [PATCH 2/8] Fix changelog --- packages/block-editor/CHANGELOG.md | 4 ++++ packages/block-library/CHANGELOG.md | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index cc14eb7d86789..827b9f2e2211e 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -7,6 +7,10 @@ - `FontSizePicker`: Update fluid utils so that only string, floats and integers are treated as valid font sizes for the purposes of fluid typography ([#44847](https://github.com/WordPress/gutenberg/pull/44847)) - `getTypographyClassesAndStyles()`: Ensure that font sizes are transformed into fluid values if fluid typography is activated ([#44852](https://github.com/WordPress/gutenberg/pull/44852)) +### New features + +- You can now drop files/blocks/HTML on unmodified default blocks to transform them into corresponding blocks ([#44647](https://github.com/WordPress/gutenberg/pull/44647)). + ## 10.2.0 (2022-10-05) ## 10.1.0 (2022-09-21) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 18c3f89875d79..54862a509e9b3 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -11,7 +11,6 @@ ### New Feature - Made it possible to import individual blocks ([#42258](https://github.com/WordPress/gutenberg/pull/42258)). Check [README](./README.md#loading-individual-blocks) for more information. -- Paragraph block: You can now drop files/blocks/HTML on an empty Paragraph block to transform it into relevant blocks ([#42722](https://github.com/WordPress/gutenberg/pull/42722)). ## 7.13.0 (2022-08-24) From 6c89fe01541e961286b5dc6a2a2d63cb99576bb1 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 4 Oct 2022 15:49:53 +0800 Subject: [PATCH 3/8] Fix animation and add e2e tests --- .../components/block-tools/insertion-point.js | 6 +- .../src/editor/index.ts | 2 + .../src/editor/set-content.ts | 22 + .../src/page-utils/drag-files.ts | 20 +- .../e2e/specs/editor/blocks/paragraph.spec.js | 412 +++++++++++++++--- 5 files changed, 404 insertions(+), 58 deletions(-) create mode 100644 packages/e2e-test-utils-playwright/src/editor/set-content.ts diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 2c5d6a8563087..2656c08303d2d 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -245,7 +245,11 @@ export default function InsertionPoint( props ) { return ( isVisible && ( insertionPoint.operation === 'replace' ? ( - + ) : ( ) ) diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 6bae1b0be5f37..32d4dff80d6aa 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -14,6 +14,7 @@ import { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; import { openPreviewPage } from './preview'; import { publishPost } from './publish-post'; import { selectBlocks } from './select-blocks'; +import { setContent } from './set-content'; import { showBlockToolbar } from './show-block-toolbar'; import { saveSiteEditorEntities } from './site-editor'; import { transformBlockTo } from './transform-block-to'; @@ -62,6 +63,7 @@ export class Editor { publishPost = publishPost.bind( this ); saveSiteEditorEntities = saveSiteEditorEntities.bind( this ); selectBlocks = selectBlocks.bind( this ); + setContent = setContent.bind( this ); showBlockToolbar = showBlockToolbar.bind( this ); transformBlockTo = transformBlockTo.bind( this ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/set-content.ts b/packages/e2e-test-utils-playwright/src/editor/set-content.ts new file mode 100644 index 0000000000000..04deaba6a1ad3 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/set-content.ts @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +/** + * Set the content of the editor. + * + * @param this + * @param html Serialized block HTML. + */ +async function setContent( this: Editor, html: string ) { + await this.page.evaluate( ( _html ) => { + // @ts-ignore (Reason: wp isn't typed). + const blocks = window.wp.blocks.parse( _html ); + + // @ts-ignore (Reason: wp isn't typed). + window.wp.data.dispatch( 'core/block-editor' ).resetBlocks( blocks ); + }, html ); +} + +export { setContent }; diff --git a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts index f8e237e7e37f1..da61143196ae1 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts @@ -9,6 +9,7 @@ import { getType } from 'mime'; * Internal dependencies */ import type { PageUtils } from './index'; +import type { Locator } from '@playwright/test'; type FileObject = { name: string; @@ -99,14 +100,19 @@ async function dragFiles( /** * Drag the files over an element (fires `dragenter` and `dragover` events). * - * @param selector A selector to search for an element. - * @param options The optional options. - * @param options.position A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. + * @param selectorOrLocator A selector or a locator to search for an element. + * @param options The optional options. + * @param options.position A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. */ - dragOver: async ( selector: string, options: Options = {} ) => { - const boundingBox = await this.page - .locator( selector ) - .boundingBox(); + dragOver: async ( + selectorOrLocator: string | Locator, + options: Options = {} + ) => { + const locator = + typeof selectorOrLocator === 'string' + ? this.page.locator( selectorOrLocator ) + : selectorOrLocator; + const boundingBox = await locator.boundingBox(); if ( ! boundingBox ) { throw new Error( diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js index 4701aa8c1a64c..3e275a8e2312a 100644 --- a/test/e2e/specs/editor/blocks/paragraph.spec.js +++ b/test/e2e/specs/editor/blocks/paragraph.spec.js @@ -8,6 +8,12 @@ const path = require( 'path' ); */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +test.use( { + draggingUtils: async ( { page }, use ) => { + await use( new DraggingUtils( { page } ) ); + }, +} ); + test.describe( 'Paragraph', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); @@ -57,6 +63,7 @@ test.describe( 'Paragraph', () => { editor, page, pageUtils, + draggingUtils, } ) => { await editor.insertBlock( { name: 'core/paragraph' } ); @@ -73,9 +80,8 @@ test.describe( 'Paragraph', () => { await dragOver( '[data-type="core/paragraph"]' ); - await expect( - page.locator( 'data-testid=block-popover-drop-zone' ) - ).toBeVisible(); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect( draggingUtils.insertionIndicator ).not.toBeVisible(); await drop(); @@ -92,6 +98,7 @@ test.describe( 'Paragraph', () => { test( 'should allow dropping blocks on en empty paragraph block', async ( { editor, page, + draggingUtils, } ) => { await editor.insertBlock( { name: 'core/heading', @@ -111,15 +118,10 @@ test.describe( 'Paragraph', () => { '[data-type="core/paragraph"][data-empty="true"]' ); const boundingBox = await emptyParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( boundingBox.x, boundingBox.y ); - } + await draggingUtils.dragOver( boundingBox.x, boundingBox.y ); - await expect( - page.locator( 'data-testid=block-popover-drop-zone' ) - ).toBeVisible(); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect( draggingUtils.insertionIndicator ).not.toBeVisible(); await page.mouse.up(); @@ -132,54 +134,20 @@ test.describe( 'Paragraph', () => { test( 'should allow dropping HTML on en empty paragraph block', async ( { editor, page, + draggingUtils, } ) => { await editor.insertBlock( { name: 'core/paragraph' } ); - // Insert a dummy draggable element on the page to simulate dragging - // HTML from other places. - await page.evaluate( () => { - const draggable = document.createElement( 'div' ); - draggable.draggable = true; - draggable.style.width = '10px'; - draggable.style.height = '10px'; - // Position it at the top left corner for convenience. - draggable.style.position = 'fixed'; - draggable.style.top = 0; - draggable.style.left = 0; - draggable.style.zIndex = 999999; - - draggable.addEventListener( - 'dragstart', - ( event ) => { - // Set the data transfer to some HTML on dragstart. - event.dataTransfer.setData( - 'text/html', - '

My Heading

' - ); - }, - { once: true } - ); - - document.body.appendChild( draggable ); - } ); - - // This is where the dummy draggable element is at. - await page.mouse.move( 0, 0 ); - await page.mouse.down(); + await draggingUtils.simulateDraggingHTML( '

My Heading

' ); const emptyParagraph = page.locator( '[data-type="core/paragraph"][data-empty="true"]' ); const boundingBox = await emptyParagraph.boundingBox(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( boundingBox.x, boundingBox.y ); - } + await draggingUtils.dragOver( boundingBox.x, boundingBox.y ); - await expect( - page.locator( 'data-testid=block-popover-drop-zone' ) - ).toBeVisible(); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect( draggingUtils.insertionIndicator ).not.toBeVisible(); await page.mouse.up(); @@ -188,5 +156,349 @@ test.describe( 'Paragraph', () => {

My Heading

` ); } ); + + test.describe( 'Dragging positions', () => { + test( 'Only the first block is an empty paragraph block', async ( { + editor, + page, + draggingUtils, + } ) => { + await editor.setContent( ` + +

+ + + +

Heading

+ + ` ); + + const emptyParagraph = page.locator( + '[data-type="core/paragraph"]' + ); + const heading = page.locator( 'text=Heading' ); + + await draggingUtils.simulateDraggingHTML( + '

Draggable

' + ); + + const firstBlockBox = await emptyParagraph.boundingBox(); + const headingBox = await heading.boundingBox(); + + { + // Dragging on the top half of an empty paragraph block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging on the bottom half of an empty paragraph block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + firstBlockBox.height - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging below the empty paragraph block but not yet on the second block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + firstBlockBox.height + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging on the top half of the heading block. + await draggingUtils.dragOver( + headingBox.x, + headingBox.y + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging on the bottom half of the heading block. + await draggingUtils.dragOver( + headingBox.x, + headingBox.y + headingBox.height - 1 + ); + await expect( draggingUtils.dropZone ).not.toBeVisible(); + await expect( + draggingUtils.insertionIndicator + ).toBeVisible(); + await expect + .poll( () => + draggingUtils.insertionIndicator + .boundingBox() + .then( ( { y, height } ) => y + height ) + ) + .toBeGreaterThan( headingBox.y + headingBox.height ); + } + } ); + + test( 'Only the second block is an empty paragraph block', async ( { + editor, + page, + draggingUtils, + } ) => { + await editor.setContent( ` + +

Heading

+ + + +

+ + ` ); + + const emptyParagraph = page.locator( + '[data-type="core/paragraph"]' + ); + const heading = page.locator( 'text=Heading' ); + + await draggingUtils.simulateDraggingHTML( + '

Draggable

' + ); + + const secondBlockBox = await emptyParagraph.boundingBox(); + const headingBox = await heading.boundingBox(); + + { + // Dragging on the top half of the heading block. + await draggingUtils.dragOver( + headingBox.x, + headingBox.y + 1 + ); + await expect( draggingUtils.dropZone ).not.toBeVisible(); + await expect( + draggingUtils.insertionIndicator + ).toBeVisible(); + await expect + .poll( () => + draggingUtils.insertionIndicator + .boundingBox() + .then( ( { y } ) => y ) + ) + .toBeLessThan( headingBox.y ); + } + + { + // Dragging on the bottom half of the heading block. + await draggingUtils.dragOver( + headingBox.x, + headingBox.y + headingBox.height - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + + { + // Dragging below the heading block but not yet on the empty paragraph block. + await draggingUtils.dragOver( + headingBox.x, + headingBox.y + headingBox.height + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + + { + // Dragging on the top half of the empty paragraph block. + await draggingUtils.dragOver( + secondBlockBox.x, + secondBlockBox.y + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + + { + // Dragging on the bottom half of the empty paragraph block. + await draggingUtils.dragOver( + secondBlockBox.x, + secondBlockBox.y + secondBlockBox.height - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + } ); + + test( 'Both blocks are empty paragraph blocks', async ( { + editor, + page, + draggingUtils, + } ) => { + await editor.setContent( ` + +

+ + + +

+ + ` ); + + const firstEmptyParagraph = page + .locator( '[data-type="core/paragraph"]' ) + .first(); + const secondEmptyParagraph = page + .locator( '[data-type="core/paragraph"]' ) + .nth( 1 ); + + await draggingUtils.simulateDraggingHTML( + '

Draggable

' + ); + + const firstBlockBox = await firstEmptyParagraph.boundingBox(); + const secondBlockBox = await secondEmptyParagraph.boundingBox(); + + { + // Dragging on the top half of the first block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging on the bottom half of the first block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + firstBlockBox.height - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging slightly below the first block but not yet on the second block. + await draggingUtils.dragOver( + firstBlockBox.x, + firstBlockBox.y + firstBlockBox.height + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( firstBlockBox ); + } + + { + // Dragging slightly above the second block but not yet on the first block. + await draggingUtils.dragOver( + secondBlockBox.x, + secondBlockBox.y - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + + { + // Dragging on the top half of the second block. + await draggingUtils.dragOver( + secondBlockBox.x, + secondBlockBox.y + 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + + { + // Dragging on the bottom half of the second block. + await draggingUtils.dragOver( + secondBlockBox.x, + secondBlockBox.y + secondBlockBox.height - 1 + ); + await expect( draggingUtils.dropZone ).toBeVisible(); + await expect + .poll( () => draggingUtils.dropZone.boundingBox() ) + .toEqual( secondBlockBox ); + } + } ); + } ); } ); } ); + +class DraggingUtils { + constructor( { page } ) { + this.page = page; + + this.dropZone = page.locator( 'data-testid=block-popover-drop-zone' ); + this.insertionIndicator = page.locator( + 'data-testid=block-list-insertion-point-indicator' + ); + } + + async dragOver( x, y ) { + // Call the move function twice to make sure the `dragOver` event is sent. + // @see https://github.com/microsoft/playwright/issues/17153 + for ( let i = 0; i < 2; i += 1 ) { + await this.page.mouse.move( x, y ); + } + } + + async simulateDraggingHTML( html ) { + // Insert a dummy draggable element on the page to simulate dragging + // HTML from other places. + await this.page.evaluate( ( _html ) => { + const draggable = document.createElement( 'div' ); + draggable.draggable = true; + draggable.style.width = '10px'; + draggable.style.height = '10px'; + // Position it at the top left corner for convenience. + draggable.style.position = 'fixed'; + draggable.style.top = 0; + draggable.style.left = 0; + draggable.style.zIndex = 999999; + + draggable.addEventListener( + 'dragstart', + ( event ) => { + // Set the data transfer to some HTML on dragstart. + event.dataTransfer.setData( 'text/html', _html ); + }, + { once: true } + ); + + document.body.appendChild( draggable ); + }, html ); + + // This is where the dummy draggable element is at. + await this.page.mouse.move( 0, 0 ); + await this.page.mouse.down(); + } +} From ef235fb7c43ce389227bbd90400b405577f97036 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 4 Oct 2022 16:26:51 +0800 Subject: [PATCH 4/8] Remove fake html draggable once the drag starts --- test/e2e/specs/editor/blocks/paragraph.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js index 3e275a8e2312a..169473c3029c8 100644 --- a/test/e2e/specs/editor/blocks/paragraph.spec.js +++ b/test/e2e/specs/editor/blocks/paragraph.spec.js @@ -473,7 +473,7 @@ class DraggingUtils { async simulateDraggingHTML( html ) { // Insert a dummy draggable element on the page to simulate dragging - // HTML from other places. + // HTML from other places. The dummy element will get removed once the drag starts. await this.page.evaluate( ( _html ) => { const draggable = document.createElement( 'div' ); draggable.draggable = true; @@ -490,6 +490,11 @@ class DraggingUtils { ( event ) => { // Set the data transfer to some HTML on dragstart. event.dataTransfer.setData( 'text/html', _html ); + + // Some browsers will cancel the drag if the source is immediately removed. + setTimeout( () => { + draggable.remove(); + }, 0 ); }, { once: true } ); From 62a8fde14e4fbd9b7fe6b952f0f1816e664975e8 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Oct 2022 11:42:51 +0800 Subject: [PATCH 5/8] Address minor reviews --- .../data/data-core-block-editor.md | 7 ++++++- .../src/components/block-popover/drop-zone.js | 4 ++-- .../components/block-tools/insertion-point.js | 8 ++++---- .../src/components/use-block-drop-zone/index.js | 17 ++++++++--------- packages/block-editor/src/store/actions.js | 13 +++++++++---- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 31e4718500018..8d7bceb5b4431 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1510,12 +1510,17 @@ _Parameters_ - _rootClientId_ `?string`: Optional root client ID of block list on which to insert. - _index_ `?number`: Index at which block should be inserted. -- _\_\_unstableOptions_ `Object`: Whether or not to show an inserter button. +- _\_\_unstableOptions_ `?Object`: Additional options. _Returns_ - `Object`: Action object. +_Properties_ + +- _\_\_unstableWithInserter_ `boolean`: Whether or not to show an inserter button. +- _operation_ `WPDropOperation`: The operation to perform when applied, either 'insert' or 'replace' for now. + ### startDraggingBlocks Returns an action object used in signalling that the user has begun to drag blocks. diff --git a/packages/block-editor/src/components/block-popover/drop-zone.js b/packages/block-editor/src/components/block-popover/drop-zone.js index 6ef3621bd9edb..c26f28127022d 100644 --- a/packages/block-editor/src/components/block-popover/drop-zone.js +++ b/packages/block-editor/src/components/block-popover/drop-zone.js @@ -17,7 +17,7 @@ const animateVariants = { exit: { opacity: 0, scaleY: 0.9 }, }; -function BlockPopoverDropZone( { +function BlockDropZonePopover( { __unstablePopoverSlot, __unstableContentRef, } ) { @@ -60,4 +60,4 @@ function BlockPopoverDropZone( { ); } -export default BlockPopoverDropZone; +export default BlockDropZonePopover; diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 2656c08303d2d..83f8edde4e45d 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -17,11 +17,11 @@ import { useReducedMotion } from '@wordpress/compose'; import Inserter from '../inserter'; import { store as blockEditorStore } from '../../store'; import BlockPopoverInbetween from '../block-popover/inbetween'; -import BlockPopoverDropZone from '../block-popover/drop-zone'; +import BlockDropZonePopover from '../block-popover/drop-zone'; export const InsertionPointOpenRef = createContext(); -function InBetweenInsertionPointPopover( { +function InbetweenInsertionPointPopover( { __unstablePopoverSlot, __unstableContentRef, } ) { @@ -245,13 +245,13 @@ export default function InsertionPoint( props ) { return ( isVisible && ( insertionPoint.operation === 'replace' ? ( - ) : ( - + ) ) ); } 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 1786c39d8740b..4f014040d66cd 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 @@ -186,17 +186,9 @@ export default function useBlockDropZone( { // Ensure the element is a block. It should have the `wp-block` class. ( element ) => element.classList.contains( 'wp-block' ) ); - // The second value in the tuple is only needed afterwards but we don't want to recalculate it. - // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const [ nearestBlockIndex, insertPosition ] = - getNearestBlockIndex( - blockElements, - { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation - ); // The block list is empty, don't show the insertion point but still allow dropping. - if ( nearestBlockIndex === undefined ) { + if ( blockElements.length === 0 ) { setDropTarget( { index: 0, operation: 'insert', @@ -204,6 +196,13 @@ export default function useBlockDropZone( { return; } + const [ nearestBlockIndex, insertPosition ] = + getNearestBlockIndex( + blockElements, + { x: event.clientX, y: event.clientY }, + getBlockListSettings( targetRootClientId )?.orientation + ); + const blocks = getBlocks( targetRootClientId ); const [ targetIndex, operation ] = getDropTargetIndexAndOperation( diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 3b9bf9c9b9f9f..971525c94dfd6 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -26,6 +26,8 @@ import { START_OF_SELECTED_AREA, } from '../utils/selection'; +/** @typedef {import('../components/use-on-block-drop/types').WPDropOperation} WPDropOperation */ + const castArray = ( maybeArray ) => Array.isArray( maybeArray ) ? maybeArray : [ maybeArray ]; @@ -624,10 +626,13 @@ export const insertBlocks = /** * Action that shows the insertion point. * - * @param {?string} rootClientId Optional root client ID of block list on - * which to insert. - * @param {?number} index Index at which block should be inserted. - * @param {Object} __unstableOptions Whether or not to show an inserter button. + * @param {?string} rootClientId Optional root client ID of block list on + * which to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?Object} __unstableOptions Additional options. + * @property {boolean} __unstableWithInserter Whether or not to show an inserter button. + * @property {WPDropOperation} operation The operation to perform when applied, + * either 'insert' or 'replace' for now. * * @return {Object} Action object. */ From 3fef816da4b73dd22aad07f7c99585a86853cbae Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Oct 2022 15:22:50 +0800 Subject: [PATCH 6/8] Add back deleted tests --- .../use-block-drop-zone/test/index.js | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) 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 index 2cda7a805023c..5cb1b17747acf 100644 --- 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 @@ -144,6 +144,42 @@ describe( 'getNearestBlockIndex', () => { expect( result ).toEqual( [ 1, 'after' ] ); } ); + it( "returns [2, 'before'] 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 ).toEqual( [ 2, 'before' ] ); + } ); + + it( "returns [2, 'after'] 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 ).toEqual( [ 2, 'after' ] ); + } ); + + it( "returns [2, 'after'] 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 ).toEqual( [ 2, 'after' ] ); + } ); + it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { const position = { x: 401, y: 0 }; @@ -220,6 +256,30 @@ describe( 'getNearestBlockIndex', () => { expect( result ).toEqual( [ 1, 'after' ] ); } ); + it( "returns [2, 'before'] 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 ).toEqual( [ 2, 'before' ] ); + } ); + + it( "returns [2, 'after'] 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 ).toEqual( [ 2, 'after' ] ); + } ); + it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { const position = { x: 0, y: 401 }; @@ -232,7 +292,7 @@ describe( 'getNearestBlockIndex', () => { expect( result ).toEqual( [ 3, 'before' ] ); } ); - it( "returns [3, 'after'] when the position is nearest to the end of the fourth block", () => { + it( "returns [3, 'after'] when the position is nearest to the end of the last block", () => { const position = { x: 300, y: 401 }; const result = getNearestBlockIndex( From 9216c4b9637eaea86895a586f6ec6408d79d4f38 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Oct 2022 18:02:49 +0800 Subject: [PATCH 7/8] Merge getNearestBlockIndex and getDropTargetIndexAndOperation --- .../components/use-block-drop-zone/index.js | 157 +++-- .../use-block-drop-zone/test/index.js | 556 +++++++++++------- 2 files changed, 426 insertions(+), 287 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 4f014040d66cd..d0700bd8d05ab 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,7 @@ import { __experimentalUseDropZone as useDropZone, } from '@wordpress/compose'; import { isRTL } from '@wordpress/i18n'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -36,16 +36,25 @@ import { store as blockEditorStore } from '../../store'; */ /** - * Given a list of block DOM elements finds the index that a block should be dropped - * at. + * @typedef {Object} WPBlockData + * @property {boolean} isUnmodifiedDefaultBlock Is the block unmodified default block. + * @property {() => DOMRect} getBoundingClientRect Get the bounding client rect of the block. + * @property {number} blockIndex The index of the block. + */ + +/** + * Get the drop target position from a given drop point and the orientation. * - * @param {Element[]} elements Array of DOM elements that represent each block in a block list. + * @param {WPBlockData[]} blocksData The block data list. * @param {WPPoint} position The position of the item being dragged. - * @param {WPBlockListOrientation} orientation The orientation of a block list. - * - * @return {[number|undefined, WPInsertPosition]} The block index and the position that's closest to the drag position. + * @param {WPBlockListOrientation} orientation The orientation of the block list. + * @return {[number, WPDropOperation]} The drop target position. */ -export function getNearestBlockIndex( elements, position, orientation ) { +export function getDropTargetPosition( + blocksData, + position, + orientation = 'vertical' +) { const allowedEdges = orientation === 'horizontal' ? [ 'left', 'right' ] @@ -53,62 +62,49 @@ export function getNearestBlockIndex( elements, position, orientation ) { const isRightToLeft = isRTL(); - let candidateIndex; - let candidatePosition = 'after'; - let candidateDistance; - - elements.forEach( ( element, index ) => { - const rect = element.getBoundingClientRect(); - - let [ distance, edge ] = getDistanceToNearestEdge( - position, - rect, - allowedEdges - ); - // Prioritize the element if the point is inside of it. - if ( isPointContainedByRect( position, rect ) ) { - distance = 0; - } + let nearestIndex = 0; + let insertPosition = 'before'; + let minDistance = Infinity; - if ( candidateDistance === undefined || distance < candidateDistance ) { - // Where the dropped block will be inserted on the nearest block. - candidatePosition = - edge === 'bottom' || - ( ! isRightToLeft && edge === 'right' ) || - ( isRightToLeft && edge === 'left' ) - ? 'after' - : 'before'; - - // Update the currently known best candidate. - candidateDistance = distance; - candidateIndex = index; - } - } ); + blocksData.forEach( + ( { isUnmodifiedDefaultBlock, getBoundingClientRect, blockIndex } ) => { + const rect = getBoundingClientRect(); - return [ candidateIndex, candidatePosition ]; -} + let [ distance, edge ] = getDistanceToNearestEdge( + position, + rect, + allowedEdges + ); + // Prioritize the element if the point is inside of an unmodified default block. + if ( + isUnmodifiedDefaultBlock && + isPointContainedByRect( position, rect ) + ) { + distance = 0; + } + + if ( distance < minDistance ) { + // Where the dropped block will be inserted on the nearest block. + insertPosition = + edge === 'bottom' || + ( ! isRightToLeft && edge === 'right' ) || + ( isRightToLeft && edge === 'left' ) + ? 'after' + : 'before'; + + // Update the currently known best candidate. + minDistance = distance; + nearestIndex = blockIndex; + } + } + ); -/** - * Get the drop target index and operation based on the the blocks and the nearst block index. - * - * @param {number|undefined} nearestIndex The nearest block index calculated by getNearestBlockIndex. - * @param {WPInsertPosition} insertPosition Whether to insert before or after the nearestIndex. - * @param {WPBlock[]} blocks The blocks list. - * @return {[number, WPDropOperation]} The drop target. - */ -export function getDropTargetIndexAndOperation( - nearestIndex, - insertPosition, - blocks -) { const adjacentIndex = nearestIndex + ( insertPosition === 'after' ? 1 : -1 ); - const nearestBlock = blocks[ nearestIndex ]; - const adjacentBlock = blocks[ adjacentIndex ]; const isNearestBlockUnmodifiedDefaultBlock = - !! nearestBlock && isUnmodifiedDefaultBlock( nearestBlock ); + !! blocksData[ nearestIndex ]?.isUnmodifiedDefaultBlock; const isAdjacentBlockUnmodifiedDefaultBlock = - !! adjacentBlock && isUnmodifiedDefaultBlock( adjacentBlock ); + !! blocksData[ adjacentIndex ]?.isUnmodifiedDefaultBlock; // If both blocks are not unmodified default blocks then just insert between them. if ( @@ -170,7 +166,8 @@ export default function useBlockDropZone( { [ targetRootClientId ] ); - const { getBlockListSettings, getBlocks } = useSelect( blockEditorStore ); + const { getBlockListSettings, getBlocks, getBlockIndex } = + useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -179,16 +176,11 @@ export default function useBlockDropZone( { } ); const throttled = useThrottle( useCallback( - ( event, currentTarget ) => { - const blockElements = Array.from( - currentTarget.children - ).filter( - // Ensure the element is a block. It should have the `wp-block` class. - ( element ) => element.classList.contains( 'wp-block' ) - ); + ( event, ownerDocument ) => { + const blocks = getBlocks( targetRootClientId ); // The block list is empty, don't show the insertion point but still allow dropping. - if ( blockElements.length === 0 ) { + if ( blocks.length === 0 ) { setDropTarget( { index: 0, operation: 'insert', @@ -196,20 +188,25 @@ export default function useBlockDropZone( { return; } - const [ nearestBlockIndex, insertPosition ] = - getNearestBlockIndex( - blockElements, - { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation - ); + const blocksData = blocks.map( ( block ) => { + const clientId = block.clientId; + + return { + isUnmodifiedDefaultBlock: + getIsUnmodifiedDefaultBlock( block ), + getBoundingClientRect: () => + ownerDocument + .getElementById( `block-${ clientId }` ) + .getBoundingClientRect(), + blockIndex: getBlockIndex( clientId ), + }; + } ); - const blocks = getBlocks( targetRootClientId ); - const [ targetIndex, operation ] = - getDropTargetIndexAndOperation( - nearestBlockIndex, - insertPosition, - blocks - ); + const [ targetIndex, operation ] = getDropTargetPosition( + blocksData, + { x: event.clientX, y: event.clientY }, + getBlockListSettings( targetRootClientId )?.orientation + ); setDropTarget( { index: targetIndex, @@ -231,7 +228,7 @@ export default function useBlockDropZone( { // `currentTarget` is only available while the event is being // handled, so get it now and pass it to the thottled function. // https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget - throttled( event, event.currentTarget ); + throttled( event, event.currentTarget.ownerDocument ); }, onDragLeave() { throttled.cancel(); 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 index 5cb1b17747acf..f5560c1cfdf13 100644 --- 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 @@ -1,18 +1,7 @@ -/** - * WordPress dependencies - */ -import { - registerBlockType, - unregisterBlockType, - createBlock, - getDefaultBlockName, - setDefaultBlockName, -} from '@wordpress/blocks'; - /** * Internal dependencies */ -import { getNearestBlockIndex, getDropTargetIndexAndOperation } from '..'; +import { getDropTargetPosition } from '..'; const elementData = [ { @@ -42,19 +31,12 @@ const elementData = [ }, ]; -const createMockClassList = ( classes ) => { - return { - contains( textToMatch ) { - return classes.includes( textToMatch ); - }, - }; -}; - const mapElements = ( orientation ) => - ( { top, right, bottom, left }, index ) => { + ( { top, right, bottom, left, isUnmodifiedDefaultBlock }, index ) => { return { - dataset: { block: index + 1 }, + isUnmodifiedDefaultBlock: !! isUnmodifiedDefaultBlock, + blockIndex: index, getBoundingClientRect() { return orientation === 'vertical' ? { @@ -70,349 +52,509 @@ const mapElements = right: bottom, }; }, - classList: createMockClassList( 'wp-block' ), }; }; -const verticalElements = elementData.map( mapElements( 'vertical' ) ); +const verticalBlocksData = elementData.map( mapElements( 'vertical' ) ); // Flip the elementData to make a horizontal block list. -const horizontalElements = elementData.map( mapElements( 'horizontal' ) ); +const horizontalBlocksData = elementData.map( mapElements( 'horizontal' ) ); -describe( 'getNearestBlockIndex', () => { - it( 'returns `undefined` for an empty list of elements', () => { - const emptyElementList = []; +describe( 'getDropTargetPosition', () => { + it( 'returns `0` for an empty list of elements', () => { const position = { x: 0, y: 0 }; const orientation = 'horizontal'; - const result = getNearestBlockIndex( - emptyElementList, - position, - orientation - ); + const result = getDropTargetPosition( [], position, orientation ); - expect( result ).toEqual( [ undefined, 'after' ] ); + expect( result ).toEqual( [ 0, 'insert' ] ); } ); describe( 'Vertical block lists', () => { const orientation = 'vertical'; - it( "returns [0, 'before'] when the position is nearest to the start of the first block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 0, 'before' ] ); + expect( result ).toEqual( [ 0, 'insert' ] ); } ); - it( "returns [0, 'after'] when the position is nearest to the end of the first block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 0, 'after' ] ); + expect( result ).toEqual( [ 1, 'insert' ] ); } ); - it( "returns [1, 'before'] when the position is nearest to the start of the second block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 1, 'before' ] ); + expect( result ).toEqual( [ 1, 'insert' ] ); } ); - it( "returns [1, 'after'] when the position is nearest to the end of the second block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 1, 'after' ] ); + expect( result ).toEqual( [ 2, 'insert' ] ); } ); - it( "returns [2, 'before'] when the position is nearest to the start of the third block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 2, 'before' ] ); + expect( result ).toEqual( [ 2, 'insert' ] ); } ); - it( "returns [2, 'after'] when the position is nearest to the end of the third block", () => { + 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, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 2, 'after' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [2, 'after'] when the position is past the end of the third block", () => { + it( 'returns `3` when the position is past the end of the third block', () => { const position = { x: 0, y: 920 }; - const result = getNearestBlockIndex( - verticalElements, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 2, 'after' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { + it( 'returns `4` when the position is nearest to the start of the fourth block', () => { const position = { x: 401, y: 0 }; - const result = getNearestBlockIndex( - verticalElements, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 3, 'before' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [3, 'after'] when the position is nearest to the end of the last block", () => { + it( 'returns `5` when the position is nearest to the end of the fourth block', () => { const position = { x: 401, y: 300 }; - const result = getNearestBlockIndex( - verticalElements, + const result = getDropTargetPosition( + verticalBlocksData, position, orientation ); - expect( result ).toEqual( [ 3, 'after' ] ); + expect( result ).toEqual( [ 4, 'insert' ] ); } ); } ); describe( 'Horizontal block lists', () => { const orientation = 'horizontal'; - it( "returns [0, 'before'] when the position is nearest to the start of the first block", () => { + 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, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 0, 'before' ] ); + expect( result ).toEqual( [ 0, 'insert' ] ); } ); - it( "returns [0, 'after'] when the position is nearest to the end of the first block", () => { + 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, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 0, 'after' ] ); + expect( result ).toEqual( [ 1, 'insert' ] ); } ); - it( "returns [1, 'before'] when the position is nearest to the start of the second block", () => { + 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, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 1, 'before' ] ); + expect( result ).toEqual( [ 1, 'insert' ] ); } ); - it( "returns [1, 'after'] when the position is nearest to the end of the second block", () => { + 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, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 1, 'after' ] ); + expect( result ).toEqual( [ 2, 'insert' ] ); } ); - it( "returns [2, 'before'] when the position is nearest to the start of the third block", () => { + 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, + const result = getDropTargetPosition( + horizontalBlocksData, + position, + orientation + ); + + expect( result ).toEqual( [ 2, 'insert' ] ); + } ); + + it( 'returns `3` when the position is nearest to the end of the third block', () => { + const position = { x: 880, y: 0 }; + + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 2, 'before' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [2, 'after'] when the position is past the end of the third block", () => { + it( 'returns `3` when the position is past the end of the third block', () => { const position = { x: 920, y: 0 }; - const result = getNearestBlockIndex( - horizontalElements, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 2, 'after' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [3, 'before'] when the position is nearest to the start of the last block", () => { + it( 'returns `3` when the position is nearest to the start of the last block', () => { const position = { x: 0, y: 401 }; - const result = getNearestBlockIndex( - horizontalElements, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 3, 'before' ] ); + expect( result ).toEqual( [ 3, 'insert' ] ); } ); - it( "returns [3, 'after'] when the position is nearest to the end of the last block", () => { + it( 'returns `4` when the position is nearest to the end of the last block', () => { const position = { x: 300, y: 401 }; - const result = getNearestBlockIndex( - horizontalElements, + const result = getDropTargetPosition( + horizontalBlocksData, position, orientation ); - expect( result ).toEqual( [ 3, 'after' ] ); + expect( result ).toEqual( [ 4, 'insert' ] ); } ); } ); -} ); -describe( 'getDropTargetIndexAndOperation', () => { - let defaultBlockName; + describe( 'Unmodified default blocks', () => { + const orientation = 'vertical'; - beforeAll( () => { - defaultBlockName = getDefaultBlockName(); - registerBlockType( 'test/default-block', { title: 'default block' } ); - registerBlockType( 'test/not-default-block', { - title: 'not default block', + it( 'handles replacement index when only the first block is an unmodified default block', () => { + const blocksData = [ + { + left: 0, + top: 10, + right: 400, + bottom: 210, + isUnmodifiedDefaultBlock: true, + }, + { + left: 0, + top: 220, + right: 400, + bottom: 420, + isUnmodifiedDefaultBlock: false, + }, + ].map( mapElements( 'vertical' ) ); + + // Dropping above the first block. + expect( + getDropTargetPosition( blocksData, { x: 0, y: 0 }, orientation ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the top half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 20 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the bottom half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 200 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping slightly after the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 211 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping slightly above the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 219 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the top half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 230 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the bottom half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 410 }, + orientation + ) + ).toEqual( [ 2, 'insert' ] ); + + // Dropping below the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 421 }, + orientation + ) + ).toEqual( [ 2, 'insert' ] ); } ); - setDefaultBlockName( 'test/default-block' ); - } ); - - afterAll( () => { - setDefaultBlockName( defaultBlockName ); - unregisterBlockType( 'test/default-block' ); - unregisterBlockType( 'test/not-default-block' ); - } ); - - it( 'returns insertion index when there are no unmodified default blocks', () => { - const blocks = [ - createBlock( 'test/not-default-block' ), - createBlock( 'test/not-default-block' ), - ]; - - expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( - [ 0, 'insert' ] - ); - - expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( - [ 1, 'insert' ] - ); - expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( - [ 1, 'insert' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( - [ 2, 'insert' ] - ); - } ); - - it( 'handles replacement index when only the first block is an unmodified default block', () => { - const blocks = [ - createBlock( 'test/default-block' ), - createBlock( 'test/not-default-block' ), - ]; - - expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( - [ 0, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( - [ 0, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( - [ 0, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( - [ 2, 'insert' ] - ); - } ); - - it( 'handles replacement index when only the second block is an unmodified default block', () => { - const blocks = [ - createBlock( 'test/not-default-block' ), - createBlock( 'test/default-block' ), - ]; - - expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( - [ 0, 'insert' ] - ); - - expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( - [ 1, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( - [ 1, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( - [ 1, 'replace' ] - ); - } ); - - it( 'returns replacement index when both blocks are unmodified default blocks', () => { - const blocks = [ - createBlock( 'test/default-block' ), - createBlock( 'test/default-block' ), - ]; - - expect( getDropTargetIndexAndOperation( 0, 'before', blocks ) ).toEqual( - [ 0, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 0, 'after', blocks ) ).toEqual( - [ 0, 'replace' ] - ); - - expect( getDropTargetIndexAndOperation( 1, 'before', blocks ) ).toEqual( - [ 1, 'replace' ] - ); + it( 'handles replacement index when only the second block is an unmodified default block', () => { + const blocksData = [ + { + left: 0, + top: 10, + right: 400, + bottom: 210, + isUnmodifiedDefaultBlock: false, + }, + { + left: 0, + top: 220, + right: 400, + bottom: 420, + isUnmodifiedDefaultBlock: true, + }, + ].map( mapElements( 'vertical' ) ); + + // Dropping above the first block. + expect( + getDropTargetPosition( blocksData, { x: 0, y: 0 }, orientation ) + ).toEqual( [ 0, 'insert' ] ); + + // Dropping on the top half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 20 }, + orientation + ) + ).toEqual( [ 0, 'insert' ] ); + + // Dropping on the bottom half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 200 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping slightly after the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 211 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping slightly above the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 219 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping on the top half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 230 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping on the bottom half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 410 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping below the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 421 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + } ); - expect( getDropTargetIndexAndOperation( 1, 'after', blocks ) ).toEqual( - [ 1, 'replace' ] - ); + it( 'returns replacement index when both blocks are unmodified default blocks', () => { + const blocksData = [ + { + left: 0, + top: 10, + right: 400, + bottom: 210, + isUnmodifiedDefaultBlock: true, + }, + { + left: 0, + top: 220, + right: 400, + bottom: 420, + isUnmodifiedDefaultBlock: true, + }, + ].map( mapElements( 'vertical' ) ); + + // Dropping above the first block. + expect( + getDropTargetPosition( blocksData, { x: 0, y: 0 }, orientation ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the top half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 20 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping on the bottom half of the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 200 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping slightly after the first block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 211 }, + orientation + ) + ).toEqual( [ 0, 'replace' ] ); + + // Dropping slightly above the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 219 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping on the top half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 230 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping on the bottom half of the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 410 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + + // Dropping below the second block. + expect( + getDropTargetPosition( + blocksData, + { x: 0, y: 421 }, + orientation + ) + ).toEqual( [ 1, 'replace' ] ); + } ); } ); } ); From b4daabc10826525d92b6b32d889cf760a41fcddc Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 13 Oct 2022 18:52:03 +0800 Subject: [PATCH 8/8] Add comment for insertion point --- .../components/block-tools/insertion-point.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 83f8edde4e45d..3b950ef87a754 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -242,16 +242,21 @@ export default function InsertionPoint( props ) { }; }, [] ); - return ( - isVisible && - ( insertionPoint.operation === 'replace' ? ( - - ) : ( - - ) ) + if ( ! isVisible ) { + return null; + } + + /** + * Render a popover that overlays the block when the desired operation is to replace it. + * Otherwise, render a popover in between blocks for the indication of inserting between them. + */ + return insertionPoint.operation === 'replace' ? ( + + ) : ( + ); }