import React, { ReactNode, useState, useEffect, forwardRef, Ref } from "react"; import styled, { StyledComponent } from "styled-components"; import { useDrag } from "react-use-gesture"; import { Spring } from "react-spring/renderprops"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; const ResizeWrapper = styled.div<{ pevents: boolean }>` display: block; & { * { pointer-events: ${(props) => !props.pevents && "none"}; } } `; const getSnappedValues = ( x: number, y: number, snapGrid: { x: number; y: number }, ) => { return { x: Math.round(x / snapGrid.x) * snapGrid.x, y: Math.round(y / snapGrid.y) * snapGrid.y, }; }; type ResizableHandleProps = { dragCallback: (x: number, y: number) => void; component: StyledComponent<"div", Record>; onStart: () => void; onStop: () => void; snapGrid: { x: number; y: number; }; }; function ResizableHandle(props: ResizableHandleProps) { const bind = useDrag( ({ first, last, dragging, movement: [mx, my], memo }) => { const snapped = getSnappedValues(mx, my, props.snapGrid); if (dragging && memo && (snapped.x !== memo.x || snapped.y !== memo.y)) { props.dragCallback(snapped.x, snapped.y); } if (first) { props.onStart(); } if (last) { props.onStop(); } return snapped; }, ); return ; } type ResizableProps = { handles: { left?: StyledComponent<"div", Record>; top?: StyledComponent<"div", Record>; bottom?: StyledComponent<"div", Record>; right?: StyledComponent<"div", Record>; bottomRight?: StyledComponent<"div", Record>; topLeft?: StyledComponent<"div", Record>; topRight?: StyledComponent<"div", Record>; bottomLeft?: StyledComponent<"div", Record>; }; componentWidth: number; componentHeight: number; children: ReactNode; onStart: () => void; onStop: ( size: { width: number; height: number }, position: { x: number; y: number }, ) => void; snapGrid: { x: number; y: number }; enable: boolean; isColliding: ( size: { width: number; height: number }, position: { x: number; y: number }, ) => boolean; className?: string; zWidgetType?: string; zWidgetId?: string; }; export const Resizable = forwardRef(function Resizable( props: ResizableProps, ref: Ref, ) { // Performance tracking start const sentryPerfTags = props.zWidgetType ? [{ name: "widget_type", value: props.zWidgetType }] : []; PerformanceTracker.startTracking( PerformanceTransactionName.SHOW_RESIZE_HANDLES, { widgetId: props.zWidgetId }, true, sentryPerfTags, ); useEffect(() => { PerformanceTracker.stopTracking( PerformanceTransactionName.SHOW_RESIZE_HANDLES, ); }); //end const [pointerEvents, togglePointerEvents] = useState(true); const [newDimensions, set] = useState({ width: props.componentWidth, height: props.componentHeight, x: 0, y: 0, reset: false, }); const setNewDimensions = (rect: { width: number; height: number; x: number; y: number; }) => { const { height, width, x, y } = rect; const isColliding = props.isColliding({ width, height }, { x, y }); if (!isColliding) { set({ ...rect, reset: false }); } }; useEffect(() => { set({ width: props.componentWidth, height: props.componentHeight, x: 0, y: 0, reset: true, }); }, [props.componentHeight, props.componentWidth]); const handles = []; if (props.handles.left) { handles.push({ dragCallback: (x: number) => { setNewDimensions({ width: props.componentWidth - x, height: newDimensions.height, x, y: newDimensions.y, }); }, component: props.handles.left, }); } if (props.handles.right) { handles.push({ dragCallback: (x: number) => { setNewDimensions({ width: props.componentWidth + x, height: newDimensions.height, x: newDimensions.x, y: newDimensions.y, }); }, component: props.handles.right, }); } if (props.handles.bottom) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: newDimensions.width, height: props.componentHeight + y, x: newDimensions.x, y: newDimensions.y, }); }, component: props.handles.bottom, }); } if (props.handles.bottomRight) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: props.componentWidth + x, height: props.componentHeight + y, x: newDimensions.x, y: newDimensions.y, }); }, component: props.handles.bottomRight, }); } if (props.handles.bottomLeft) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: props.componentWidth - x, height: props.componentHeight + y, x, y: newDimensions.y, }); }, component: props.handles.bottomLeft, }); } if (props.handles.topRight) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: props.componentWidth + x, height: props.componentHeight - y, x: newDimensions.x, y: y, }); }, component: props.handles.topRight, }); } if (props.handles.topLeft) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: props.componentWidth - x, height: props.componentHeight - y, x: x, y: y, }); }, component: props.handles.topLeft, }); } if (props.handles.top) { handles.push({ dragCallback: (x: number, y: number) => { setNewDimensions({ width: newDimensions.width, height: props.componentHeight - y, y: y, x: newDimensions.x, }); }, component: props.handles.top, }); } const onResizeStop = () => { togglePointerEvents(true); props.onStop( { width: newDimensions.width, height: newDimensions.height, }, { x: newDimensions.x, y: newDimensions.y, }, ); }; const renderHandles = handles.map((handle, index) => ( { togglePointerEvents(false); props.onStart(); }} onStop={onResizeStop} snapGrid={props.snapGrid} /> )); return ( {(_props) => ( {props.children} {props.enable && renderHandles} )} ); }); export default Resizable;