UNPKG

@syncfusion/react-popups

Version:

A package of React popup components such as Tooltip that is used to display information or messages in separate pop-ups.

371 lines (370 loc) 15.6 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useRef, useState, useCallback, useEffect } from 'react'; import { useProviderContext } from '@syncfusion/react-base'; const DEFAULT_MIN_SIZE = 100; const BORDER_HANDLE_SIZE = 2; /** * Specifies a custom hook that provides resize functionality for React components. * Allows elements to be resized by dragging handles on the edges and corners, * with support for minimum/maximum size constraints and multiple resize directions. * * @param {React.RefObject<HTMLElement>} elementRef - Reference to the element that will be made resizable * @param {IResize} options - Resize configuration options including constraints and event handlers * @returns {IResizeContext} Resize state and handle rendering function */ export function useResize(elementRef, options) { const { enabled = false, handles = ['SouthEast'], minWidth = DEFAULT_MIN_SIZE, minHeight = DEFAULT_MIN_SIZE, maxWidth = Number.MAX_SAFE_INTEGER, maxHeight = Number.MAX_SAFE_INTEGER, boundary, onResizeStart, onResize, onResizeStop } = options; const { dir } = useProviderContext(); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); const currentDirectionRef = useRef(undefined); const originalStateRef = useRef({ width: 0, height: 0, left: 0, top: 0, mouseX: 0, mouseY: 0 }); const limitsRef = useRef({ minWidthPx: DEFAULT_MIN_SIZE, minHeightPx: DEFAULT_MIN_SIZE, maxWidthPx: Number.MAX_SAFE_INTEGER, maxHeightPx: Number.MAX_SAFE_INTEGER }); const allDirections = ['North', 'East', 'South', 'West', 'NorthEast', 'NorthWest', 'SouthEast', 'SouthWest']; const parseToPixels = useCallback((value, defaultValue, referenceSize) => { if (value === undefined) { return defaultValue; } if (typeof value === 'number') { return value; } if (typeof value !== 'string') { return defaultValue; } if (value.endsWith('%')) { const percentage = parseFloat(value) / 100; return referenceSize * percentage; } if (value.endsWith('px')) { return parseFloat(value); } if (elementRef.current && typeof window !== 'undefined') { const computedStyle = window.getComputedStyle(elementRef.current); const fontSize = parseFloat(computedStyle.fontSize); const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); if (value.endsWith('em')) { return parseFloat(value) * fontSize; } if (value.endsWith('rem')) { return parseFloat(value) * rootFontSize; } if (value.endsWith('vh')) { return (parseFloat(value) / 100) * window.innerHeight; } if (value.endsWith('vw')) { return (parseFloat(value) / 100) * window.innerWidth; } if (value.endsWith('vmin')) { const minV = Math.min(window.innerWidth, window.innerHeight); return (parseFloat(value) / 100) * minV; } if (value.endsWith('vmax')) { const maxV = Math.max(window.innerWidth, window.innerHeight); return (parseFloat(value) / 100) * maxV; } if (value.endsWith('ch')) { return parseFloat(value) * fontSize * 0.5; } if (value.endsWith('ex')) { return parseFloat(value) * fontSize * 0.5; } } return defaultValue; }, [elementRef]); const updateResizeLimits = useCallback(() => { if (!elementRef.current) { return; } const parentElement = elementRef.current.parentElement || document.body; const parentRect = parentElement.getBoundingClientRect(); limitsRef.current = { minWidthPx: parseToPixels(minWidth, DEFAULT_MIN_SIZE, parentRect.width), minHeightPx: parseToPixels(minHeight, DEFAULT_MIN_SIZE, parentRect.height), maxWidthPx: parseToPixels(maxWidth, Number.MAX_SAFE_INTEGER, parentRect.width), maxHeightPx: parseToPixels(maxHeight, Number.MAX_SAFE_INTEGER, parentRect.height) }; }, [minWidth, minHeight, maxWidth, maxHeight, parseToPixels]); const isCardinalDirection = useCallback((direction) => { return ['North', 'South', 'East', 'West'].includes(direction); }, []); const getActiveHandles = useCallback(() => { if (!enabled) { return []; } const mapRtlDirections = (direction) => { const rtlMap = { 'East': 'West', 'West': 'East', 'NorthEast': 'NorthWest', 'NorthWest': 'NorthEast', 'SouthEast': 'SouthWest', 'SouthWest': 'SouthEast', 'North': 'North', 'South': 'South', 'All': 'All' }; return rtlMap[direction]; }; let activeHandles; if (handles.includes('All')) { activeHandles = [...allDirections]; } else { activeHandles = handles?.length > 0 ? handles.filter((dir) => allDirections.includes(dir)) : ['SouthEast']; } if (dir === 'rtl') { return activeHandles.map(mapRtlDirections); } return activeHandles; }, [enabled, handles, dir]); useEffect(() => { if (elementRef.current && enabled) { const rect = elementRef.current.getBoundingClientRect(); setWidth(rect.width); setHeight(rect.height); updateResizeLimits(); } }, [elementRef.current, enabled, updateResizeLimits]); useEffect(() => { if (enabled) { updateResizeLimits(); } }, [minWidth, minHeight, maxWidth, maxHeight, enabled, updateResizeLimits]); const handleResizeStart = useCallback((e, direction) => { if (!enabled || !elementRef.current) { return; } if (!('touches' in e)) { e.preventDefault(); } updateResizeLimits(); const rect = elementRef.current.getBoundingClientRect(); originalStateRef.current.width = rect.width; originalStateRef.current.height = rect.height; originalStateRef.current.left = rect.left; originalStateRef.current.top = rect.top; let clientX; let clientY; if ('changedTouches' in e) { clientX = e.changedTouches[0].clientX; clientY = e.changedTouches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } originalStateRef.current.mouseX = clientX; originalStateRef.current.mouseY = clientY; currentDirectionRef.current = direction; if (onResizeStart && elementRef.current) { const rect = elementRef.current.getBoundingClientRect(); const resizeEvent = { event: e, width: rect.width, height: rect.height, direction: currentDirectionRef.current, cancel: false }; onResizeStart(resizeEvent); if (resizeEvent.cancel) { return; } } document.addEventListener('mousemove', handleResize); document.addEventListener('touchmove', handleResize); document.addEventListener('mouseup', handleResizeEnd); document.addEventListener('touchend', handleResizeEnd); }, [enabled, onResizeStart, elementRef, boundary, updateResizeLimits]); const handleResize = useCallback((e) => { if (!elementRef.current) { return; } if (!('touches' in e)) { e.preventDefault(); } let clientX; let clientY; if ('changedTouches' in e) { clientX = e.changedTouches[0].clientX; clientY = e.changedTouches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const deltaX = clientX - originalStateRef.current.mouseX; const deltaY = clientY - originalStateRef.current.mouseY; let newWidth = originalStateRef.current.width; let newHeight = originalStateRef.current.height; let newLeft = originalStateRef.current.left; let newTop = originalStateRef.current.top; const direction = currentDirectionRef.current; const { minWidthPx, maxWidthPx, minHeightPx, maxHeightPx } = limitsRef.current; if (direction.includes('East')) { newWidth = Math.max(minWidthPx, Math.min(maxWidthPx, originalStateRef.current.width + deltaX)); } if (direction.includes('West')) { const widthChange = -deltaX; newWidth = Math.max(minWidthPx, Math.min(maxWidthPx, originalStateRef.current.width + widthChange)); if (newWidth !== originalStateRef.current.width) { newLeft = originalStateRef.current.left - (newWidth - originalStateRef.current.width); } } if (direction.includes('South')) { newHeight = Math.max(minHeightPx, Math.min(maxHeightPx, originalStateRef.current.height + deltaY)); } if (direction.includes('North')) { const heightChange = -deltaY; newHeight = Math.max(minHeightPx, Math.min(maxHeightPx, originalStateRef.current.height + heightChange)); if (newHeight !== originalStateRef.current.height) { newTop = originalStateRef.current.top - (newHeight - originalStateRef.current.height); } } if (boundary && boundary !== document.body) { const boundaryRect = boundary.getBoundingClientRect(); if (newLeft < boundaryRect.left) { newLeft = boundaryRect.left; newWidth = originalStateRef.current.width - (boundaryRect.left - originalStateRef.current.left); } if (newTop < boundaryRect.top) { newTop = boundaryRect.top; newHeight = originalStateRef.current.height - (boundaryRect.top - originalStateRef.current.top); } if (newLeft + newWidth > boundaryRect.right) { newWidth = boundaryRect.right - newLeft; } if (newTop + newHeight > boundaryRect.bottom) { newHeight = boundaryRect.bottom - newTop; } } setWidth(newWidth); setHeight(newHeight); if (elementRef.current) { elementRef.current.style.width = `${newWidth}px`; elementRef.current.style.height = `${newHeight}px`; elementRef.current.style.left = `${newLeft}px`; elementRef.current.style.top = `${newTop}px`; if (boundary && boundary !== document.body) { elementRef.current.style.position = 'fixed'; } else { elementRef.current.style.position = 'absolute'; } } if (onResize) { onResize({ event: e, width: newWidth, height: newHeight, direction: direction }); } }, [onResize, boundary]); const handleResizeEnd = useCallback((e) => { if (onResizeStop && elementRef.current) { const rect = elementRef.current.getBoundingClientRect(); onResizeStop({ event: e, width: rect.width, height: rect.height, direction: currentDirectionRef.current }); } document.removeEventListener('mousemove', handleResize); document.removeEventListener('touchmove', handleResize); document.removeEventListener('mouseup', handleResizeEnd); document.removeEventListener('touchend', handleResizeEnd); }, [onResizeStop, handleResize]); useEffect(() => { return () => { document.removeEventListener('mousemove', handleResize); document.removeEventListener('touchmove', handleResize); document.removeEventListener('mouseup', handleResizeEnd); document.removeEventListener('touchend', handleResizeEnd); }; }, [handleResize, handleResizeEnd]); const getHandleProps = useCallback((direction) => { const directionToClassMap = { 'North': 'north sf-resize-ns', 'South': 'south sf-resize-ns', 'East': 'east sf-resize-ew', 'West': 'west sf-resize-ew', 'NorthEast': 'north-east sf-resize-nesw', 'NorthWest': 'north-west sf-resize-nwse', 'SouthEast': 'south-east sf-resize-nwse', 'SouthWest': 'south-west sf-resize-nesw', 'All': 'all' }; const directionClass = directionToClassMap[direction]; const isCardinal = isCardinalDirection(direction); const baseStyle = { position: 'absolute', zIndex: 1000 }; let positionStyle = {}; if (isCardinal) { if (direction === 'North') { positionStyle = { height: `${BORDER_HANDLE_SIZE}px`, width: '100%', top: '0', left: '0' }; } else if (direction === 'South') { positionStyle = { height: `${BORDER_HANDLE_SIZE}px`, width: '100%', bottom: '0', left: '0' }; } else if (direction === 'East') { positionStyle = { width: `${BORDER_HANDLE_SIZE}px`, height: '100%', right: '0', top: '0' }; } else if (direction === 'West') { positionStyle = { width: `${BORDER_HANDLE_SIZE}px`, height: '100%', left: '0', top: '0' }; } } return { className: isCardinal ? `sf-dlg-border-resize sf-dlg-${directionClass}` : `sf-dlg-resize-handle sf-dlg-${directionClass}`, ...(isCardinal ? { style: { ...baseStyle, ...positionStyle } } : {}), onMouseDown: (e) => handleResizeStart(e, direction), onTouchStart: (e) => handleResizeStart(e, direction), role: 'button', 'aria-label': `Resize ${direction}` }; }, [handleResizeStart, isCardinalDirection]); const renderResizeHandles = useCallback((iconComponent) => { const handles = getActiveHandles(); return handles.map((direction) => { const props = getHandleProps(direction); const isCorner = !isCardinalDirection(direction); const ElementType = isCardinalDirection(direction) ? 'span' : 'div'; return (_jsx(ElementType, { ...props, children: isCorner && iconComponent }, direction)); }); }, [getActiveHandles, getHandleProps, isCardinalDirection]); return { width, height, direction: currentDirectionRef.current, renderResizeHandles }; }