import { useRef, useEffect, useState } from '@wordpress/element'; import classNames from 'classnames/dedupe'; import { useDispatch } from '@wordpress/data'; import SizeIcon from 'blockbite-icons/dist/Size'; import MoveIcon from 'blockbite-icons/dist/Move'; import ArrowUpIcon from 'blockbite-icons/dist/ArrowUp'; import ArrowDownIcon from 'blockbite-icons/dist/ArrowDown'; import useMousePosition from './useMousePosition'; import EyeOpenIcon from 'blockbite-icons/dist/EyeOpen'; import EyeClosedIcon from 'blockbite-icons/dist/EyeClosed'; import type { PositionProps } from './AreaHelpers'; import has from 'lodash/has'; import { biteStyleToPosition, getAreaModifiers } from './AreaHelpers'; import type { BiteStyleProps } from '@components/DesignPanel/types'; import { getNewDesignClasses, getMergedDesignList, } from '@components/DesignPanel/Helpers'; export type mousePositionProps = { x: number; y: number; }; export type AreaBlockProps = { clientId: string; key: string; isCanvas: boolean; isActive: boolean; className?: string; gridSizeX: number; gridSizeY: number; gridGapX: number; gridGapY: number; currentResponsive: string; setActiveBlockId: (id: string) => void; biteStyle: BiteStyleProps[]; }; export default function AreaBlock({ clientId, isCanvas, isActive, gridSizeX, gridSizeY, gridGapX, gridGapY, currentResponsive, setActiveBlockId, biteStyle, }: AreaBlockProps) { const ref = useRef(null); const moveHandleRef = useRef(null); const resizeHandleRef = useRef(null); const [isResizing, setIsResizing] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isVisible, setIsVisible] = useState( !(biteStyle.find((style) => style.id === 'overflow')?.value === 'hidden') ); const [position, setPosition] = useState({ x: null, y: null, w: null, h: null, }); const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0, w: 0, h: 0, }); const mousePosition = useMousePosition(); const targetRef = useRef(null); const { getBlockRootClientId, getBlockIndex, getBlocks } = wp.data.select('core/editor'); const { moveBlockToPosition } = useDispatch('core/block-editor'); const buttonWidth = 32.8; const halfButtonWidth = buttonWidth / 2; /* iniate block position */ useEffect(() => { if (clientId) { targetRef.current = document.getElementById(`block-${clientId}`); // biteStyleToPosition returns an object fromout the classnames b_area-x-2 b_area-y-1 b_area-w-2 b_area-h-5 // for example pos: { x : 2, y: 1, w :2, h : 5} const { pos } = biteStyleToPosition(biteStyle); const gridToPercent = convertGridToPercent(pos); setPosition(gridToPercent); } }, [clientId, gridGapX, gridGapY, gridSizeX, gridSizeY]); useEffect(() => { if (isResizing && isCanvas) { handleResize(mousePosition); setTargetRefPosition(); } if (isDragging && isCanvas) { handleDrag(mousePosition); setTargetRefPosition(); } }, [mousePosition.x, mousePosition.y]); /* * Canvas Release Handler * Reset dragging and resizing state * Update block biteStyle and className */ useEffect(() => { if (!isCanvas) { setIsDragging(false); setIsResizing(false); update(); } }, [isCanvas]); const update = () => { if ( position.x === null || position.y === null || position.w === null || position.h === null ) { return; } const percentToGrid = convertPercentToGrid(position); const updatedAreaStyles = [] as BiteStyleProps[]; getAreaModifiers().forEach((style) => { // safely pick style.position value from percentToGrid let value = null; if (has(percentToGrid, style.position)) { value = percentToGrid[style.position].toString(); } else if (style.id === 'b_area') { value = 'b_area'; } else if (style.id === 'overflow' && isVisible) { value = isVisible ? 'hidden' : 'visible'; } if (value) { updatedAreaStyles.push({ id: style.id, screen: currentResponsive, value: value, }); } }); const updatedBiteStyles = getMergedDesignList(biteStyle, updatedAreaStyles); targetRef.current.style.removeProperty('top'); targetRef.current.style.removeProperty('left'); targetRef.current.style.removeProperty('width'); targetRef.current.style.removeProperty('height'); targetRef.current.style.removeProperty('position'); const biteClass = getNewDesignClasses(updatedBiteStyles); // check if biteclass contains NaN in any string if (biteClass.includes('NaN')) { return; } blockbite.updateBlockAttributes(clientId, { biteStyle: updatedBiteStyles, biteClass: biteClass, }); }; const setTargetRefPosition = () => { const style = { left: `${position.x}%`, top: `${position.y}%`, width: `${position.w}%`, height: `${position.h}%`, position: 'absolute', }; targetRef.current.classList.remove('b_area'); targetRef.current.style.setProperty('top', style.top, 'important'); targetRef.current.style.setProperty('left', style.left, 'important'); targetRef.current.style.setProperty('width', style.width, 'important'); targetRef.current.style.setProperty('height', style.height, 'important'); targetRef.current.style.setProperty( 'position', style.position, 'important' ); }; const getParentCoordinates = () => { const parentWidth = ref.current.parentElement.offsetWidth; const parentHeight = ref.current.parentElement.offsetHeight; const totalGridGapX = gridGapX * (gridSizeX - 1); const totalGridGapY = gridGapY * (gridSizeY - 1); const gridWidth = Math.round((parentWidth - totalGridGapX) / gridSizeX); const gridHeight = Math.round((parentHeight - totalGridGapY) / gridSizeY); return { parentWidth, parentHeight, parentLeft: ref.current.parentElement.getBoundingClientRect().left, parentTop: ref.current.parentElement.getBoundingClientRect().top, gridWidth, gridHeight, }; }; const convertPercentToPixels = (percentPosition: PositionProps) => { const { parentWidth, parentHeight } = getParentCoordinates(); if (!percentPosition) { return null; } return { x: (percentPosition.x / 100) * parentWidth, y: (percentPosition.y / 100) * parentHeight, w: (percentPosition.w / 100) * parentWidth, h: (percentPosition.h / 100) * parentHeight, }; }; const convertPercentToGrid = (percentPosition: any) => { const { parentWidth, parentHeight, gridWidth, gridHeight } = getParentCoordinates(); if (!percentPosition) { return null; } const pixelPosition = { x: (percentPosition.x / 100) * parentWidth, y: (percentPosition.y / 100) * parentHeight, w: (percentPosition.w / 100) * parentWidth, h: (percentPosition.h / 100) * parentHeight, }; // Adjust pixel values to grid units, accounting for 1-based indexing const gridPosition = { x: Math.round(pixelPosition.x / (gridWidth + gridGapX)) + 1, y: Math.round(pixelPosition.y / (gridHeight + gridGapY)) + 1, w: Math.round(pixelPosition.w / (gridWidth + gridGapX)), h: Math.round(pixelPosition.h / (gridHeight + gridGapY)), }; return gridPosition; }; const convertGridToPercent = (gridPosition: PositionProps) => { const { parentWidth, parentHeight, gridWidth, gridHeight } = getParentCoordinates(); const adjustedGridPosition = { x: gridPosition.x - 1, // Convert from 1-based to 0-based index for calculation y: gridPosition.y - 1, w: gridPosition.w, h: gridPosition.h, }; const pixelPosition = { x: adjustedGridPosition.x * gridWidth + adjustedGridPosition.x * gridGapX, y: adjustedGridPosition.y * gridHeight + adjustedGridPosition.y * gridGapY, w: adjustedGridPosition.w * gridWidth + (adjustedGridPosition.w - 1) * gridGapX, h: adjustedGridPosition.h * gridHeight + (adjustedGridPosition.h - 1) * gridGapY, }; const percentPosition = { x: (pixelPosition.x / parentWidth) * 100, y: (pixelPosition.y / parentHeight) * 100, w: (pixelPosition.w / parentWidth) * 100, h: (pixelPosition.h / parentHeight) * 100, }; return percentPosition; }; const handleDrag = (mousePosition: mousePositionProps) => { const { parentWidth, parentHeight, parentLeft, parentTop, gridWidth, gridHeight, } = getParentCoordinates(); const dragLeft = Math.round(mousePosition.x - parentLeft); const dragTop = Math.round(mousePosition.y - parentTop); // Calculate how many columns are crossed and adjust the position const columnsCrossedX = Math.floor(dragLeft / (gridWidth + gridGapX)); const columnsCrossedY = Math.floor(dragTop / (gridHeight + gridGapY)); const snapLeft = columnsCrossedX * (gridWidth + gridGapX); const snapTop = columnsCrossedY * (gridHeight + gridGapY); const dragLeftPercent = (snapLeft / parentWidth) * 100; const dragTopPercent = (snapTop / parentHeight) * 100; const pos = { ...position }; pos.x = dragLeftPercent; pos.y = dragTopPercent; setPosition(pos); }; const handleResize = (mousePosition: mousePositionProps) => { const { parentWidth, parentHeight, parentLeft, parentTop, gridWidth, gridHeight, } = getParentCoordinates(); const mouseX = mousePosition.x + halfButtonWidth - parentLeft; const mouseY = mousePosition.y + halfButtonWidth - parentTop; // Convert initial position from percent to pixels const percentToPixels = convertPercentToPixels(initialPosition); const initialPixelPosition = percentToPixels; // Calculate new dimensions in pixels based on the mouse position const newWidth = mouseX - initialPixelPosition.x; const newHeight = mouseY - initialPixelPosition.y; // Align new dimensions to the grid while considering gaps const resizeWidthGrid = Math.round(newWidth / (gridWidth + gridGapX)) * (gridWidth + gridGapX); const resizeHeightGrid = Math.round(newHeight / (gridHeight + gridGapY)) * (gridHeight + gridGapY); // Calculate how many columns/rows are crossed during resize const columnsCrossedWidth = Math.floor( resizeWidthGrid / (gridWidth + gridGapX) ); const rowsCrossedHeight = Math.floor( resizeHeightGrid / (gridHeight + gridGapY) ); // Adjust the new position to snap to the end of the last column/row const widthPercentage = ((columnsCrossedWidth * gridWidth + (columnsCrossedWidth - 1) * gridGapX) / parentWidth) * 100; const heightPercentage = ((rowsCrossedHeight * gridHeight + (rowsCrossedHeight - 1) * gridGapY) / parentHeight) * 100; // Update the position with the new dimensions in percentages const newPosition = { ...initialPosition, w: widthPercentage, h: heightPercentage, }; setPosition(newPosition); }; function handleMoveBlock(direction: 'up' | 'down') { // Get all the blocks in the current block's root. const blocks = getBlocks(getBlockRootClientId(clientId)); if (blocks.length < 2) { // There's only one block, nothing to move. return; } // Get the target block depending on the direction. // Target block is the block above or below the current block. const targetBlock = direction === 'up' ? blocks[getBlockIndex(clientId) + 1] : blocks[getBlockIndex(clientId) - 1]; if (!targetBlock) { // There's no block to move to. return; } const targetBlockClientId = targetBlock.clientId; const targetBlockRootClientId = getBlockRootClientId(targetBlockClientId); const sourceClientId = clientId; const sourceRootClientId = getBlockRootClientId(clientId); const fromRootClientId = sourceRootClientId; const toRootClientId = targetBlockRootClientId; const targetIndex = getBlockIndex(targetBlockClientId); moveBlockToPosition( sourceClientId, fromRootClientId, toRootClientId, targetIndex ); } function handleVisibility() { setIsVisible(!isVisible); } const setHandleResize = (e: any) => { // Only handle left mouse button if (e.button !== 0) return; setInitialPosition({ x: position.x, y: position.y, w: position.w, h: position.h, }); setIsResizing(true); }; const setHandleDrag = (e: any) => { // Only handle left mouse button if (e.button !== 0) return; setInitialPosition({ x: position.x, y: position.y, w: position.w, h: position.h, }); setIsDragging(true); }; return (
setActiveBlockId(clientId)} >
); }