@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
JavaScript
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
};
}