@wordpress/block-editor
Version:
370 lines (369 loc) • 12.6 kB
JavaScript
// packages/block-editor/src/components/use-block-drop-zone/index.js
import { useDispatch, useSelect, useRegistry } from "@wordpress/data";
import { useCallback, useState } from "@wordpress/element";
import {
useThrottle,
__experimentalUseDropZone as useDropZone
} from "@wordpress/compose";
import { isRTL } from "@wordpress/i18n";
import {
hasBlockSupport,
isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock,
store as blocksStore
} from "@wordpress/blocks";
import useOnBlockDrop from "../use-on-block-drop";
import {
getDistanceToNearestEdge,
isPointContainedByRect,
isPointWithinTopAndBottomBoundariesOfRect
} from "../../utils/math";
import { store as blockEditorStore } from "../../store";
import { unlock } from "../../lock-unlock";
var THRESHOLD_DISTANCE = 30;
var MINIMUM_HEIGHT_FOR_THRESHOLD = 120;
var MINIMUM_WIDTH_FOR_THRESHOLD = 120;
function getDropTargetPosition(blocksData, position, orientation = "vertical", options = {}) {
const allowedEdges = orientation === "horizontal" ? ["left", "right"] : ["top", "bottom"];
let nearestIndex = 0;
let insertPosition = "before";
let minDistance = Infinity;
let targetBlockIndex = null;
let nearestSide = "right";
const {
dropZoneElement,
parentBlockOrientation,
rootBlockIndex = 0
} = options;
if (dropZoneElement && parentBlockOrientation !== "horizontal") {
const rect = dropZoneElement.getBoundingClientRect();
const [distance, edge] = getDistanceToNearestEdge(position, rect, [
"top",
"bottom"
]);
if (rect.height > MINIMUM_HEIGHT_FOR_THRESHOLD && distance < THRESHOLD_DISTANCE) {
if (edge === "top") {
return [rootBlockIndex, "before"];
}
if (edge === "bottom") {
return [rootBlockIndex + 1, "after"];
}
}
}
const isRightToLeft = isRTL();
if (dropZoneElement && parentBlockOrientation === "horizontal") {
const rect = dropZoneElement.getBoundingClientRect();
const [distance, edge] = getDistanceToNearestEdge(position, rect, [
"left",
"right"
]);
if (rect.width > MINIMUM_WIDTH_FOR_THRESHOLD && distance < THRESHOLD_DISTANCE) {
if (isRightToLeft && edge === "right" || !isRightToLeft && edge === "left") {
return [rootBlockIndex, "before"];
}
if (isRightToLeft && edge === "left" || !isRightToLeft && edge === "right") {
return [rootBlockIndex + 1, "after"];
}
}
}
blocksData.forEach(
({
isUnmodifiedDefaultBlock,
getBoundingClientRect,
blockIndex,
blockOrientation
}) => {
const rect = getBoundingClientRect();
if (!rect) {
return;
}
let [distance, edge] = getDistanceToNearestEdge(
position,
rect,
allowedEdges
);
const [sideDistance, sideEdge] = getDistanceToNearestEdge(
position,
rect,
["left", "right"]
);
const isPointInsideRect = isPointContainedByRect(position, rect);
if (isUnmodifiedDefaultBlock && isPointInsideRect) {
distance = 0;
} else if (orientation === "vertical" && blockOrientation !== "horizontal" && (isPointInsideRect && sideDistance < THRESHOLD_DISTANCE || !isPointInsideRect && isPointWithinTopAndBottomBoundariesOfRect(
position,
rect
))) {
targetBlockIndex = blockIndex;
nearestSide = sideEdge;
}
if (distance < minDistance) {
insertPosition = edge === "bottom" || !isRightToLeft && edge === "right" || isRightToLeft && edge === "left" ? "after" : "before";
minDistance = distance;
nearestIndex = blockIndex;
}
}
);
const adjacentIndex = nearestIndex + (insertPosition === "after" ? 1 : -1);
const isNearestBlockUnmodifiedDefaultBlock = !!blocksData[nearestIndex]?.isUnmodifiedDefaultBlock;
const isAdjacentBlockUnmodifiedDefaultBlock = !!blocksData[adjacentIndex]?.isUnmodifiedDefaultBlock;
if (targetBlockIndex !== null) {
return [targetBlockIndex, "group", nearestSide];
}
if (!isNearestBlockUnmodifiedDefaultBlock && !isAdjacentBlockUnmodifiedDefaultBlock) {
const insertionIndex = insertPosition === "after" ? nearestIndex + 1 : nearestIndex;
return [insertionIndex, "insert"];
}
return [
isNearestBlockUnmodifiedDefaultBlock ? nearestIndex : adjacentIndex,
"replace"
];
}
function isDropTargetValid(getBlockType, allowedBlocks, draggedBlockNames, targetBlockName) {
let areBlocksAllowed = true;
if (allowedBlocks) {
const allowedBlockNames = allowedBlocks?.map(({ name }) => name);
areBlocksAllowed = draggedBlockNames.every(
(name) => allowedBlockNames?.includes(name)
);
}
const draggedBlockTypes = draggedBlockNames.map(
(name) => getBlockType(name)
);
const targetMatchesDraggedBlockParents = draggedBlockTypes.every(
(block) => {
const [allowedParentName] = block?.parent || [];
if (!allowedParentName) {
return true;
}
return allowedParentName === targetBlockName;
}
);
return areBlocksAllowed && targetMatchesDraggedBlockParents;
}
function isInsertionPoint(targetToCheck, ownerDocument) {
const { defaultView } = ownerDocument;
return !!(defaultView && targetToCheck instanceof defaultView.HTMLElement && targetToCheck.closest("[data-is-insertion-point]"));
}
function useBlockDropZone({
dropZoneElement,
// An undefined value represents a top-level block. Default to an empty
// string for this so that `targetRootClientId` can be easily compared to
// values returned by the `getRootBlockClientId` selector, which also uses
// an empty string to represent top-level blocks.
rootClientId: targetRootClientId = "",
parentClientId: parentBlockClientId = "",
isDisabled = false
} = {}) {
const registry = useRegistry();
const [dropTarget, setDropTarget] = useState({
index: null,
operation: "insert"
});
const { getBlockType, getBlockVariations, getGroupingBlockName } = useSelect(blocksStore);
const {
canInsertBlockType,
getBlockListSettings,
getBlocks,
getBlockIndex,
getDraggedBlockClientIds,
getBlockNamesByClientId,
getAllowedBlocks,
isDragging,
isGroupable,
isZoomOut,
getSectionRootClientId,
getBlockParents
} = unlock(useSelect(blockEditorStore));
const {
showInsertionPoint,
hideInsertionPoint,
startDragging,
stopDragging
} = unlock(useDispatch(blockEditorStore));
const onBlockDrop = useOnBlockDrop(
dropTarget.operation === "before" || dropTarget.operation === "after" ? parentBlockClientId : targetRootClientId,
dropTarget.index,
{
operation: dropTarget.operation,
nearestSide: dropTarget.nearestSide
}
);
const throttled = useThrottle(
useCallback(
(event, ownerDocument) => {
if (!isDragging()) {
startDragging();
}
const draggedBlockClientIds = getDraggedBlockClientIds();
const targetParents = [
targetRootClientId,
...getBlockParents(targetRootClientId, true)
];
const isTargetWithinDraggedBlocks = draggedBlockClientIds.some(
(clientId) => targetParents.includes(clientId)
);
if (isTargetWithinDraggedBlocks) {
return;
}
const allowedBlocks = getAllowedBlocks(targetRootClientId);
const targetBlockName = getBlockNamesByClientId([
targetRootClientId
])[0];
const draggedBlockNames = getBlockNamesByClientId(
draggedBlockClientIds
);
const isBlockDroppingAllowed = isDropTargetValid(
getBlockType,
allowedBlocks,
draggedBlockNames,
targetBlockName
);
if (!isBlockDroppingAllowed) {
return;
}
const sectionRootClientId = getSectionRootClientId();
if (isZoomOut() && sectionRootClientId !== targetRootClientId) {
return;
}
const blocks = getBlocks(targetRootClientId).filter((block) => {
return !(hasBlockSupport(block.name, "visibility", true) && block.attributes?.metadata?.blockVisibility === false);
});
if (blocks.length === 0) {
registry.batch(() => {
setDropTarget({
index: 0,
operation: "insert"
});
showInsertionPoint(targetRootClientId, 0, {
operation: "insert"
});
});
return;
}
const blocksData = blocks.map((block) => {
const clientId = block.clientId;
return {
isUnmodifiedDefaultBlock: getIsUnmodifiedDefaultBlock(block),
getBoundingClientRect: () => {
const blockElement = ownerDocument.getElementById(
`block-${clientId}`
);
return blockElement ? blockElement.getBoundingClientRect() : null;
},
blockIndex: getBlockIndex(clientId),
blockOrientation: getBlockListSettings(clientId)?.orientation
};
});
const dropTargetPosition = getDropTargetPosition(
blocksData,
{ x: event.clientX, y: event.clientY },
getBlockListSettings(targetRootClientId)?.orientation,
{
dropZoneElement,
parentBlockClientId,
parentBlockOrientation: parentBlockClientId ? getBlockListSettings(parentBlockClientId)?.orientation : void 0,
rootBlockIndex: getBlockIndex(targetRootClientId)
}
);
const [targetIndex, operation, nearestSide] = dropTargetPosition;
const isTargetIndexEmptyDefaultBlock = blocksData[targetIndex]?.isUnmodifiedDefaultBlock;
if (isZoomOut() && !isTargetIndexEmptyDefaultBlock && operation !== "insert") {
return;
}
if (operation === "group") {
const targetBlock = blocks[targetIndex];
const areAllImages = [
targetBlock.name,
...draggedBlockNames
].every((name) => name === "core/image");
const canInsertGalleryBlock = canInsertBlockType(
"core/gallery",
targetRootClientId
);
const areGroupableBlocks = isGroupable([
targetBlock.clientId,
getDraggedBlockClientIds()
]);
const groupBlockVariations = getBlockVariations(
getGroupingBlockName(),
"block"
);
const canInsertRow = groupBlockVariations && groupBlockVariations.find(
({ name }) => name === "group-row"
);
if (areAllImages && !canInsertGalleryBlock && (!areGroupableBlocks || !canInsertRow)) {
return;
}
if (!areAllImages && (!areGroupableBlocks || !canInsertRow)) {
return;
}
}
registry.batch(() => {
setDropTarget({
index: targetIndex,
operation,
nearestSide
});
const insertionPointClientId = [
"before",
"after"
].includes(operation) ? parentBlockClientId : targetRootClientId;
showInsertionPoint(insertionPointClientId, targetIndex, {
operation,
nearestSide
});
});
},
[
isDragging,
getAllowedBlocks,
targetRootClientId,
getBlockNamesByClientId,
getDraggedBlockClientIds,
getBlockType,
getSectionRootClientId,
isZoomOut,
getBlocks,
getBlockListSettings,
dropZoneElement,
parentBlockClientId,
getBlockIndex,
registry,
startDragging,
showInsertionPoint,
canInsertBlockType,
isGroupable,
getBlockVariations,
getGroupingBlockName
]
),
200
);
return useDropZone({
dropZoneElement,
isDisabled,
onDrop: onBlockDrop,
onDragOver(event) {
throttled(event, event.currentTarget.ownerDocument);
},
onDragLeave(event) {
const { ownerDocument } = event.currentTarget;
if (isInsertionPoint(event.relatedTarget, ownerDocument) || isInsertionPoint(event.target, ownerDocument)) {
return;
}
throttled.cancel();
hideInsertionPoint();
},
onDragEnd() {
throttled.cancel();
stopDragging();
hideInsertionPoint();
}
});
}
export {
useBlockDropZone as default,
getDropTargetPosition,
isDropTargetValid
};
//# sourceMappingURL=index.js.map