UNPKG

@wordpress/block-editor

Version:
370 lines (369 loc) 12.6 kB
// 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