UNPKG

@wordpress/block-editor

Version:
396 lines (370 loc) 16.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useBlockDropZone; exports.getDropTargetPosition = getDropTargetPosition; exports.isDropTargetValid = isDropTargetValid; var _data = require("@wordpress/data"); var _element = require("@wordpress/element"); var _compose = require("@wordpress/compose"); var _i18n = require("@wordpress/i18n"); var _blocks = require("@wordpress/blocks"); var _useOnBlockDrop = _interopRequireDefault(require("../use-on-block-drop")); var _math = require("../../utils/math"); var _store = require("../../store"); var _lockUnlock = require("../../lock-unlock"); /** * WordPress dependencies */ /** * Internal dependencies */ const THRESHOLD_DISTANCE = 30; const MINIMUM_HEIGHT_FOR_THRESHOLD = 120; const MINIMUM_WIDTH_FOR_THRESHOLD = 120; /** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** @typedef {import('../use-on-block-drop/types').WPDropOperation} WPDropOperation */ /** * The orientation of a block list. * * @typedef {'horizontal'|'vertical'|undefined} WPBlockListOrientation */ /** * The insert position when dropping a block. * * @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. * * @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. * @param {Object} options Additional options. * @return {[number, WPDropOperation]} The drop target position. */ 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; // Allow before/after when dragging over the top/bottom edges of the drop zone. if (dropZoneElement && parentBlockOrientation !== 'horizontal') { const rect = dropZoneElement.getBoundingClientRect(); const [distance, edge] = (0, _math.getDistanceToNearestEdge)(position, rect, ['top', 'bottom']); // If dragging over the top or bottom of the drop zone, insert the block // before or after the parent block. This only applies to blocks that use // a drop zone element, typically container blocks such as Group or Cover. 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 = (0, _i18n.isRTL)(); // Allow before/after when dragging over the left/right edges of the drop zone. if (dropZoneElement && parentBlockOrientation === 'horizontal') { const rect = dropZoneElement.getBoundingClientRect(); const [distance, edge] = (0, _math.getDistanceToNearestEdge)(position, rect, ['left', 'right']); // If dragging over the left or right of the drop zone, insert the block // before or after the parent block. This only applies to blocks that use // a drop zone element, typically container blocks such as Group. 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(); let [distance, edge] = (0, _math.getDistanceToNearestEdge)(position, rect, allowedEdges); // If the the point is close to a side, prioritize that side. const [sideDistance, sideEdge] = (0, _math.getDistanceToNearestEdge)(position, rect, ['left', 'right']); const isPointInsideRect = (0, _math.isPointContainedByRect)(position, rect); // Prioritize the element if the point is inside of an unmodified default block. if (isUnmodifiedDefaultBlock && isPointInsideRect) { distance = 0; } else if (orientation === 'vertical' && blockOrientation !== 'horizontal' && (isPointInsideRect && sideDistance < THRESHOLD_DISTANCE || !isPointInsideRect && (0, _math.isPointWithinTopAndBottomBoundariesOfRect)(position, rect))) { /** * This condition should only apply when the layout is vertical (otherwise there's * no need to create a Row) and dropzones should only activate when the block is * either within and close to the sides of the target block or on its outer sides. */ targetBlockIndex = blockIndex; nearestSide = sideEdge; } 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 the target index is set then group with the block at that index. if (targetBlockIndex !== null) { return [targetBlockIndex, 'group', nearestSide]; } // 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']; } /** * Check if the dragged blocks can be dropped on the target. * @param {Function} getBlockType * @param {Object[]} allowedBlocks * @param {string[]} draggedBlockNames * @param {string} targetBlockName * @return {boolean} Whether the dragged blocks can be dropped on the target. */ function isDropTargetValid(getBlockType, allowedBlocks, draggedBlockNames, targetBlockName) { // At root level allowedBlocks is undefined and all blocks are allowed. // Otherwise, check if all dragged blocks are allowed. let areBlocksAllowed = true; if (allowedBlocks) { const allowedBlockNames = allowedBlocks?.map(({ name }) => name); areBlocksAllowed = draggedBlockNames.every(name => allowedBlockNames?.includes(name)); } // Work out if dragged blocks have an allowed parent and if so // check target block matches the allowed parent. 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; } /** * Checks if the given element is an insertion point. * * @param {EventTarget|null} targetToCheck - The element to check. * @param {Document} ownerDocument - The owner document of the element. * @return {boolean} True if the element is a insertion point, false otherwise. */ function isInsertionPoint(targetToCheck, ownerDocument) { const { defaultView } = ownerDocument; return !!(defaultView && targetToCheck instanceof defaultView.HTMLElement && targetToCheck.closest('[data-is-insertion-point]')); } /** * @typedef {Object} WPBlockDropZoneConfig * @property {?HTMLElement} dropZoneElement Optional element to be used as the drop zone. * @property {string} rootClientId The root client id for the block list. */ /** * A React hook that can be used to make a block list handle drag and drop. * * @param {WPBlockDropZoneConfig} dropZoneConfig configuration data for the drop zone. */ 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 = (0, _data.useRegistry)(); const [dropTarget, setDropTarget] = (0, _element.useState)({ index: null, operation: 'insert' }); const { getBlockType, getBlockVariations, getGroupingBlockName } = (0, _data.useSelect)(_blocks.store); const { canInsertBlockType, getBlockListSettings, getBlocks, getBlockIndex, getDraggedBlockClientIds, getBlockNamesByClientId, getAllowedBlocks, isDragging, isGroupable, isZoomOut, getSectionRootClientId, getBlockParents } = (0, _lockUnlock.unlock)((0, _data.useSelect)(_store.store)); const { showInsertionPoint, hideInsertionPoint, startDragging, stopDragging } = (0, _lockUnlock.unlock)((0, _data.useDispatch)(_store.store)); const onBlockDrop = (0, _useOnBlockDrop.default)(dropTarget.operation === 'before' || dropTarget.operation === 'after' ? parentBlockClientId : targetRootClientId, dropTarget.index, { operation: dropTarget.operation, nearestSide: dropTarget.nearestSide }); const throttled = (0, _compose.useThrottle)((0, _element.useCallback)((event, ownerDocument) => { if (!isDragging()) { // When dragging from the desktop, no drag start event is fired. // So, ensure that the drag state is set when the user drags over a drop zone. startDragging(); } const draggedBlockClientIds = getDraggedBlockClientIds(); const targetParents = [targetRootClientId, ...getBlockParents(targetRootClientId, true)]; // Check if the target is within any of the dragged blocks. 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(); // In Zoom Out mode, if the target is not the section root provided by settings then // do not allow dropping as the drop target is not within the root (that which is // treated as "the content" by Zoom Out Mode). if (isZoomOut() && sectionRootClientId !== targetRootClientId) { return; } const blocks = getBlocks(targetRootClientId); // The block list is empty, don't show the insertion point but still allow dropping. 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: (0, _blocks.isUnmodifiedDefaultBlock)(block), getBoundingClientRect: () => ownerDocument.getElementById(`block-${clientId}`).getBoundingClientRect(), 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 : undefined, 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 the dragged blocks and the target block are all images, // check if it is creatable either a Row variation or a Gallery block. if (areAllImages && !canInsertGalleryBlock && (!areGroupableBlocks || !canInsertRow)) { return; } // If the dragged blocks and the target block are not all images, // check if it is creatable a Row variation. 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 (0, _compose.__experimentalUseDropZone)({ dropZoneElement, isDisabled, onDrop: onBlockDrop, onDragOver(event) { // `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.ownerDocument); }, onDragLeave(event) { const { ownerDocument } = event.currentTarget; // If the drag event is leaving the drop zone and entering an insertion point, // do not hide the insertion point as it is conceptually within the dropzone. if (isInsertionPoint(event.relatedTarget, ownerDocument) || isInsertionPoint(event.target, ownerDocument)) { return; } throttled.cancel(); hideInsertionPoint(); }, onDragEnd() { throttled.cancel(); stopDragging(); hideInsertionPoint(); } }); } //# sourceMappingURL=index.js.map