diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index 31e4718500018e..8d7bceb5b44317 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/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index cc14eb7d867897..827b9f2e2211e4 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-editor/src/components/block-popover/drop-zone.js b/packages/block-editor/src/components/block-popover/drop-zone.js
new file mode 100644
index 00000000000000..c26f28127022d6
--- /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 BlockDropZonePopover( {
+ __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 BlockDropZonePopover;
diff --git a/packages/block-editor/src/components/block-popover/style.scss b/packages/block-editor/src/components/block-popover/style.scss
index 1ed4774a56c010..1744506179e670 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 4f98a29a6e0efb..3b950ef87a7544 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 BlockDropZonePopover from '../block-popover/drop-zone';
export const InsertionPointOpenRef = createContext();
-function InsertionPointPopover( {
+function InbetweenInsertionPointPopover( {
__unstablePopoverSlot,
__unstableContentRef,
} ) {
@@ -232,9 +233,30 @@ 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 && ;
+ 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' ? (
+
+ ) : (
+
+ );
}
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index 0ad7de536282e2..6b7a24887b3423 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 731ae2530ec114..346631667c2544 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 3bef31546c711e..d0700bd8d05abb 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 as getIsUnmodifiedDefaultBlock } 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.
@@ -25,16 +30,31 @@ import { store as blockEditorStore } from '../../store';
*/
/**
- * Given a list of block DOM elements finds the index that a block should be dropped
- * at.
+ * The insert position when dropping a block.
*
- * @param {Element[]} elements Array of DOM elements that represent each block in a block list.
- * @param {WPPoint} position The position of the item being dragged.
- * @param {WPBlockListOrientation} orientation The orientation of a block list.
+ * @typedef {'before'|'after'} WPInsertPosition
+ */
+
+/**
+ * @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.
*
- * @return {number|undefined} The block index that's closest to the drag position.
+ * @param {WPBlockData[]} blocksData The block data list.
+ * @param {WPPoint} position The position of the item being dragged.
+ * @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' ]
@@ -42,49 +62,67 @@ export function getNearestBlockIndex( elements, position, orientation ) {
const isRightToLeft = isRTL();
- let candidateIndex;
- let candidateDistance;
-
- elements.forEach( ( element, index ) => {
- const rect = element.getBoundingClientRect();
- const [ distance, edge ] = getDistanceToNearestEdge(
- position,
- rect,
- allowedEdges
- );
-
- 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 =
- edge === 'bottom' ||
- ( ! isRightToLeft && edge === 'right' ) ||
- ( isRightToLeft && edge === 'left' );
- const offset = isTrailingEdge ? 1 : 0;
-
- // Update the currently known best candidate.
- candidateDistance = distance;
- candidateIndex = index + offset;
- }
- } );
+ let nearestIndex = 0;
+ let insertPosition = 'before';
+ let minDistance = Infinity;
- return candidateIndex;
-}
+ blocksData.forEach(
+ ( { isUnmodifiedDefaultBlock, getBoundingClientRect, blockIndex } ) => {
+ const rect = getBoundingClientRect();
-/**
- * Determine if the element is an empty paragraph block.
- *
- * @param {?HTMLElement} element The element being tested.
- * @return {boolean} True or False.
- */
-function isEmptyParagraph( element ) {
- return (
- !! element &&
- element.dataset.type === 'core/paragraph' &&
- element.dataset.empty === 'true'
+ 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;
+ }
+ }
);
+
+ const adjacentIndex =
+ nearestIndex + ( insertPosition === 'after' ? 1 : -1 );
+ const isNearestBlockUnmodifiedDefaultBlock =
+ !! blocksData[ nearestIndex ]?.isUnmodifiedDefaultBlock;
+ const isAdjacentBlockUnmodifiedDefaultBlock =
+ !! blocksData[ adjacentIndex ]?.isUnmodifiedDefaultBlock;
+
+ // 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 +142,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 +166,58 @@ export default function useBlockDropZone( {
[ targetRootClientId ]
);
- const { getBlockListSettings } = useSelect( blockEditorStore );
+ const { getBlockListSettings, getBlocks, getBlockIndex } =
+ 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, ownerDocument ) => {
+ const blocks = getBlocks( targetRootClientId );
+
+ // The block list is empty, don't show the insertion point but still allow dropping.
+ if ( blocks.length === 0 ) {
+ setDropTarget( {
+ index: 0,
+ operation: 'insert',
+ } );
return;
}
- showInsertionPoint( targetRootClientId, targetIndex );
- }
- }, [] ),
+ const blocksData = blocks.map( ( block ) => {
+ const clientId = block.clientId;
+
+ return {
+ isUnmodifiedDefaultBlock:
+ getIsUnmodifiedDefaultBlock( block ),
+ getBoundingClientRect: () =>
+ ownerDocument
+ .getElementById( `block-${ clientId }` )
+ .getBoundingClientRect(),
+ blockIndex: getBlockIndex( clientId ),
+ };
+ } );
+
+ const [ targetIndex, operation ] = getDropTargetPosition(
+ blocksData,
+ { x: event.clientX, y: event.clientY },
+ getBlockListSettings( targetRootClientId )?.orientation
+ );
+
+ setDropTarget( {
+ index: targetIndex,
+ operation,
+ } );
+ showInsertionPoint( targetRootClientId, targetIndex, {
+ operation,
+ } );
+ },
+ [ targetRootClientId ]
+ ),
200
);
@@ -169,17 +228,15 @@ 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();
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 492f89ff7467a0..f5560c1cfdf13a 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,7 @@
/**
* Internal dependencies
*/
-import { getNearestBlockIndex } from '..';
+import { getDropTargetPosition } from '..';
const elementData = [
{
@@ -31,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'
? {
@@ -59,27 +52,21 @@ 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 ).toBeUndefined();
+ expect( result ).toEqual( [ 0, 'insert' ] );
} );
describe( 'Vertical block lists', () => {
@@ -88,109 +75,109 @@ describe( 'getNearestBlockIndex', () => {
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 ).toBe( 0 );
+ expect( result ).toEqual( [ 0, 'insert' ] );
} );
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 ).toBe( 1 );
+ expect( result ).toEqual( [ 1, 'insert' ] );
} );
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 ).toBe( 1 );
+ expect( result ).toEqual( [ 1, 'insert' ] );
} );
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 ).toBe( 2 );
+ expect( result ).toEqual( [ 2, 'insert' ] );
} );
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 ).toBe( 2 );
+ expect( result ).toEqual( [ 2, 'insert' ] );
} );
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 ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
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 ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
- it( 'returns `3` when the position is nearest to the start of the fourth 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 ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
- it( 'returns `4` when the position is nearest to the end of the fourth 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 ).toBe( 4 );
+ expect( result ).toEqual( [ 4, 'insert' ] );
} );
} );
@@ -200,109 +187,374 @@ describe( 'getNearestBlockIndex', () => {
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 ).toBe( 0 );
+ expect( result ).toEqual( [ 0, 'insert' ] );
} );
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 ).toBe( 1 );
+ expect( result ).toEqual( [ 1, 'insert' ] );
} );
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 ).toBe( 1 );
+ expect( result ).toEqual( [ 1, 'insert' ] );
} );
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 ).toBe( 2 );
+ expect( result ).toEqual( [ 2, 'insert' ] );
} );
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 ).toBe( 2 );
+ 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 = getNearestBlockIndex(
- horizontalElements,
+ const result = getDropTargetPosition(
+ horizontalBlocksData,
position,
orientation
);
- expect( result ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
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 ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
- it( 'returns `3` when the position is nearest to the start of the fourth 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 ).toBe( 3 );
+ expect( result ).toEqual( [ 3, 'insert' ] );
} );
- it( 'returns `4` when the position is nearest to the end of the fourth 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 ).toBe( 4 );
+ expect( result ).toEqual( [ 4, 'insert' ] );
+ } );
+ } );
+
+ describe( 'Unmodified default blocks', () => {
+ const orientation = 'vertical';
+
+ 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' ] );
+ } );
+
+ 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' ] );
+ } );
+
+ 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' ] );
} );
} );
} );
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 245f912bcf4da7..6b0a9300124493 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 00000000000000..1b24ea7e90f9a6
--- /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 68b6f61ca89d63..971525c94dfd65 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.
*/
@@ -636,12 +641,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 e306829c22ed55..3c04134f8ffd83 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 f5140f446c6a12..128972e8a400e1 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/CHANGELOG.md b/packages/block-library/CHANGELOG.md
index 18c3f89875d797..54862a509e9b3f 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)
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 e51fb84acf8062..00000000000000
--- 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 c049b0b72bd249..5340eb5e4ea543 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 && (
-
- ) }
{
+ // @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 f8e237e7e37f1d..da61143196ae1c 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/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts
index 22ccf83ff41fcc..ac68ae7630e314 100644
--- a/packages/e2e-test-utils-playwright/src/test.ts
+++ b/packages/e2e-test-utils-playwright/src/test.ts
@@ -137,6 +137,11 @@ const test = base.extend<
await Promise.all( [
requestUtils.activateTheme( 'twentytwentyone' ),
+ // Disable this test plugin as it's conflicting with some of the tests.
+ // We already have reduced motion enabled and Playwright will wait for most of the animations anyway.
+ requestUtils.deactivatePlugin(
+ 'gutenberg-test-plugin-disables-the-css-animations'
+ ),
requestUtils.deleteAllPosts(),
requestUtils.deleteAllBlocks(),
requestUtils.resetPreferences(),
diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js
index 6a043153320d83..169473c3029c81 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=empty-paragraph-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=empty-paragraph-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=empty-paragraph-drop-zone' )
- ).toBeVisible();
+ await expect( draggingUtils.dropZone ).toBeVisible();
+ await expect( draggingUtils.insertionIndicator ).not.toBeVisible();
await page.mouse.up();
@@ -188,5 +156,354 @@ 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. The dummy element will get removed once the drag starts.
+ 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 );
+
+ // Some browsers will cancel the drag if the source is immediately removed.
+ setTimeout( () => {
+ draggable.remove();
+ }, 0 );
+ },
+ { 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();
+ }
+}