Files
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

225 lines
6.7 KiB
TypeScript
Executable File

import {RefObject} from 'react';
import {
interactableEvent,
InteractableEvent,
InteractableRect,
} from './interactable-event';
import {usePointerEvents, UsePointerEventsProps} from './use-pointer-events';
import {activeInteraction, setActiveInteraction} from './active-interaction';
import {calcNewSizeFromAspectRatio} from './utils/calc-new-size-from-aspect-ratio';
import {restrictResizableWithinBoundary} from './utils/restrict-resizable-within-boundary';
import {domRectToObj} from './utils/dom-rect-to-obj';
export enum resizeHandlePosition {
topLeft = 'topLeft',
topRight = 'topRight',
bottomLeft = 'bottomLeft',
bottomRight = 'bottomRight',
}
type AspectRatio = number | null | undefined;
type InitialAspectRatio = AspectRatio | 'initial';
interface ResizeState {
currentRect?: InteractableRect;
resizeDir?: resizeHandlePosition;
initialAspectRatio?: AspectRatio;
boundaryRect?: InteractableRect;
}
let state: ResizeState = {};
const resetState = (value: ResizeState = {}) => {
setActiveInteraction(null);
state = value;
};
interface UseResizeProps {
boundaryRect?: InteractableRect;
boundaryRef?: RefObject<HTMLElement> | null;
restrictWithinBoundary?: boolean;
aspectRatio?: InitialAspectRatio;
onResizeStart?: (e: InteractableEvent) => void;
onResize?: (e: InteractableEvent) => void;
onResizeEnd?: (e: InteractableEvent) => void;
minWidth?: number;
minHeight?: number;
}
export function useResize({
aspectRatio,
boundaryRef,
boundaryRect,
restrictWithinBoundary = true,
minWidth = 50,
minHeight = 50,
...props
}: UseResizeProps) {
const pointerProps: UsePointerEventsProps = {
onMoveStart: (e, resizable) => {
const target = e.target as HTMLElement;
if (!target.dataset.resizeHandle || activeInteraction) {
return false;
}
resetState({
currentRect: domRectToObj(resizable.getBoundingClientRect()),
resizeDir: target.dataset.resizeHandle as resizeHandlePosition,
});
if (!state.currentRect) {
return false;
}
setActiveInteraction('resize');
if (boundaryRect) {
state.boundaryRect = boundaryRect;
} else if (boundaryRef?.current) {
state.boundaryRect = domRectToObj(
boundaryRef.current.getBoundingClientRect(),
);
}
// if we have a boundary, x, y will be relative to that boundary, otherwise it will be relative to window
if (state.currentRect && state.boundaryRect) {
state.currentRect.left -= state.boundaryRect.left;
state.currentRect.top -= state.boundaryRect.top;
}
state.initialAspectRatio =
state.currentRect.width / state.currentRect!.height;
props.onResizeStart?.(interactableEvent({rect: state.currentRect, e}));
},
onMove: (e, deltaX, deltaY) => {
if (!state.resizeDir || !state.currentRect) return;
const ratio =
aspectRatio === 'initial' ? state.initialAspectRatio : aspectRatio;
const newRect = resizeRect(state.currentRect, deltaX, deltaY, ratio);
const boundedRect = applyBounds(newRect, minWidth, minHeight, ratio);
props.onResize?.(
interactableEvent({rect: boundedRect, e, deltaX, deltaY}),
);
state.currentRect = newRect;
},
onMoveEnd: e => {
if (state.currentRect) {
props.onResizeEnd?.(interactableEvent({rect: state.currentRect, e}));
}
resetState();
},
};
const {domProps} = usePointerEvents(pointerProps);
return {resizeProps: domProps};
}
function resizeRect(
rect: InteractableRect,
deltaX: number,
deltaY: number,
ratio: AspectRatio,
): InteractableRect {
const prevRect = {...rect};
const newRect = {...rect};
if (state.resizeDir === resizeHandlePosition.topRight) {
newRect.width = Math.floor(newRect.width + deltaX);
if (ratio) {
newRect.height = Math.floor(newRect.width / ratio);
} else {
newRect.height = Math.floor(newRect.height - deltaY);
}
newRect.top = Math.floor(newRect.top + (prevRect.height - newRect.height));
} else if (state.resizeDir === resizeHandlePosition.bottomRight) {
newRect.width = Math.floor(newRect.width + deltaX);
if (ratio) {
newRect.height = Math.floor(newRect.width / ratio);
} else {
newRect.height = Math.floor(newRect.height + deltaY);
}
} else if (state.resizeDir === resizeHandlePosition.topLeft) {
newRect.width = Math.floor(newRect.width - deltaX);
if (ratio) {
newRect.height = Math.floor(newRect.width / ratio);
} else {
newRect.height = Math.floor(newRect.height - deltaY);
}
newRect.left = Math.floor(newRect.left + (prevRect.width - newRect.width));
newRect.top = Math.floor(newRect.top + (prevRect.height - newRect.height));
} else if (state.resizeDir === resizeHandlePosition.bottomLeft) {
newRect.width = Math.floor(newRect.width - deltaX);
if (ratio) {
newRect.height = Math.floor(newRect.width / ratio);
} else {
newRect.height = Math.floor(newRect.height + deltaY);
}
newRect.left = Math.floor(newRect.left + deltaX);
}
return newRect;
}
function applyBounds(
rect: InteractableRect,
minWidth: number,
minHeight: number,
ratio: AspectRatio,
): InteractableRect {
const isLeftSideHandle =
state.resizeDir === resizeHandlePosition.bottomLeft ||
state.resizeDir === resizeHandlePosition.topLeft;
const isTopSideHandle =
state.resizeDir === resizeHandlePosition.topRight ||
state.resizeDir === resizeHandlePosition.topLeft;
// bound width and height
let boundedRect = {...rect};
boundedRect.width = Math.max(minWidth, rect.width);
boundedRect.height = Math.max(minHeight, rect.height);
// compensate left when width is bounded
const widthRestriction = boundedRect.width - rect.width;
if (isLeftSideHandle && widthRestriction > 0) {
boundedRect.left -= widthRestriction;
}
// compensate top when height is bounded
const heightRestriction = boundedRect.height - rect.height;
if (isTopSideHandle && heightRestriction > 0) {
boundedRect.top -= heightRestriction;
}
if (state.boundaryRect) {
boundedRect = restrictResizableWithinBoundary(
boundedRect,
state.boundaryRect,
);
}
if (ratio) {
// adjust width/height based on specified aspect ratio
const oldWidth = boundedRect.width;
const oldHeight = boundedRect.height;
const size = calcNewSizeFromAspectRatio(
ratio,
boundedRect.width,
boundedRect.height,
);
boundedRect.width = size.width;
boundedRect.height = size.height;
// compensate top/left that was bound previously
if (isTopSideHandle) {
boundedRect.top += oldHeight - boundedRect.height;
}
if (isLeftSideHandle) {
boundedRect.left += oldWidth - boundedRect.width;
}
}
return boundedRect;
}