@adaptabletools/adaptable
Version:
Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
311 lines (310 loc) • 13.3 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import * as React from 'react';
import { Resizable } from 're-resizable';
import useDraggable from '../utils/useDraggable';
import { isBrowserDocumentAvailable } from '../../View/UIHelper';
import { createPortal } from 'react-dom';
import { useStacking } from './useStacking';
import { clamp } from '../../Utilities/Extensions/NumberExtensions';
let portalElement;
const ensurePortalElement = () => {
if (!isBrowserDocumentAvailable()) {
return;
}
if (portalElement) {
return;
}
portalElement = document.createElement('div');
portalElement.classList.add('ab-cmp-modal-window');
document.body.appendChild(portalElement);
};
const normalizeTopLeft = (position, edgeOffset) => position < edgeOffset ? edgeOffset : position;
export const WINDOW_MODAL_VIEWPORT_EDGE_OFFSET = 10;
const getViewportSize = () => {
if (!isBrowserDocumentAvailable()) {
return { width: Infinity, height: Infinity };
}
return { width: window.innerWidth, height: window.innerHeight };
};
const parsePxConstraint = (value) => {
if (!value) {
return undefined;
}
const match = /^(-?\d+(?:\.\d+)?)px$/.exec(value.trim());
if (!match) {
return undefined;
}
const parsed = Number(match[1]);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
};
const readDomSizeConstraints = (element) => {
if (!element || !isBrowserDocumentAvailable()) {
return {};
}
const computed = window.getComputedStyle(element);
return {
minWidth: parsePxConstraint(computed.minWidth),
minHeight: parsePxConstraint(computed.minHeight),
maxWidth: parsePxConstraint(computed.maxWidth),
maxHeight: parsePxConstraint(computed.maxHeight),
};
};
export const clampWindowModalToViewport = (position, size, minWidth = 0, minHeight = 0, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
const { width: viewportWidth, height: viewportHeight } = getViewportSize();
const width = clamp(size.width, minWidth, Math.max(minWidth, viewportWidth - 2 * edgeOffset));
const height = clamp(size.height, minHeight, Math.max(minHeight, viewportHeight - 2 * edgeOffset));
const x = clamp(position.x, edgeOffset, Math.max(edgeOffset, viewportWidth - edgeOffset - width));
const y = clamp(position.y, edgeOffset, Math.max(edgeOffset, viewportHeight - edgeOffset - height));
return {
position: { x, y },
size: { width, height },
};
};
export const clampWindowModalPositionToViewport = (position, size, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
const { width: viewportWidth, height: viewportHeight } = getViewportSize();
return {
x: clamp(position.x, edgeOffset, Math.max(edgeOffset, viewportWidth - edgeOffset - size.width)),
y: clamp(position.y, edgeOffset, Math.max(edgeOffset, viewportHeight - edgeOffset - size.height)),
};
};
const getResizePositionDelta = (direction, delta) => {
const positionDelta = { x: 0, y: 0 };
const left = -delta.width;
const top = -delta.height;
const directions = ['top', 'left', 'topLeft', 'bottomLeft', 'topRight'];
if (directions.indexOf(direction) !== -1) {
if (direction === 'bottomLeft') {
positionDelta.x += left;
}
else if (direction === 'topRight') {
positionDelta.y += top;
}
else {
positionDelta.x += left;
positionDelta.y += top;
}
return positionDelta;
}
return null;
};
const resizeDirectionAffects = (direction) => ({
left: ['left', 'topLeft', 'bottomLeft'].includes(direction),
top: ['top', 'topLeft', 'topRight'].includes(direction),
right: ['right', 'topRight', 'bottomRight'].includes(direction),
bottom: ['bottom', 'bottomLeft', 'bottomRight'].includes(direction),
});
const getClampedResizeState = (direction, delta, basePosition, baseSize, minWidth, minHeight, maxWidth, maxHeight, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
const { width: viewportWidth, height: viewportHeight } = getViewportSize();
const affects = resizeDirectionAffects(direction);
let deltaWidth = delta.width;
let deltaHeight = delta.height;
if (affects.left) {
deltaWidth = Math.min(deltaWidth, Math.max(0, basePosition.x - edgeOffset));
}
if (affects.top) {
deltaHeight = Math.min(deltaHeight, Math.max(0, basePosition.y - edgeOffset));
}
if (affects.right) {
deltaWidth = Math.min(deltaWidth, viewportWidth - edgeOffset - basePosition.x - baseSize.width);
}
if (affects.bottom) {
deltaHeight = Math.min(deltaHeight, viewportHeight - edgeOffset - basePosition.y - baseSize.height);
}
if (affects.left || affects.right) {
const maxDeltaWidth = maxWidth != null ? maxWidth - baseSize.width : Infinity;
const minDeltaWidth = minWidth - baseSize.width;
if (minDeltaWidth <= maxDeltaWidth) {
deltaWidth = clamp(deltaWidth, minDeltaWidth, maxDeltaWidth);
}
}
if (affects.top || affects.bottom) {
const maxDeltaHeight = maxHeight != null ? maxHeight - baseSize.height : Infinity;
const minDeltaHeight = minHeight - baseSize.height;
if (minDeltaHeight <= maxDeltaHeight) {
deltaHeight = clamp(deltaHeight, minDeltaHeight, maxDeltaHeight);
}
}
const adjustedDelta = { width: deltaWidth, height: deltaHeight };
const positionDelta = getResizePositionDelta(direction, adjustedDelta);
const newPosition = {
x: basePosition.x + (positionDelta?.x ?? 0),
y: basePosition.y + (positionDelta?.y ?? 0),
};
const newSize = {
width: baseSize.width + deltaWidth,
height: baseSize.height + deltaHeight,
};
const clamped = clampWindowModalToViewport(newPosition, newSize, minWidth, minHeight, edgeOffset);
return {
...clamped,
positionDelta: positionDelta
? {
x: clamped.position.x - basePosition.x,
y: clamped.position.y - basePosition.y,
}
: null,
};
};
const RESIZE_HANDLE_CLASS_PREFIX = 'ab-WindowModal__resize-handle';
const RESIZE_EDGE_HANDLES = ['left', 'right', 'top', 'bottom'];
const RESIZE_HANDLE_CLASSES = {
left: `${RESIZE_HANDLE_CLASS_PREFIX}--left`,
right: `${RESIZE_HANDLE_CLASS_PREFIX}--right`,
top: `${RESIZE_HANDLE_CLASS_PREFIX}--top`,
bottom: `${RESIZE_HANDLE_CLASS_PREFIX}--bottom`,
};
const AT_CONSTRAINT_EPSILON = 0.5;
const GROW_CURSOR = {
left: 'w-resize',
right: 'e-resize',
top: 'n-resize',
bottom: 's-resize',
};
const SHRINK_CURSOR = {
left: 'e-resize',
right: 'w-resize',
top: 's-resize',
bottom: 'n-resize',
};
const getEdgeResizeCursor = (edge, size, constraints) => {
const isHorizontal = edge === 'left' || edge === 'right';
const value = isHorizontal ? size.width : size.height;
const min = isHorizontal ? constraints.minWidth : constraints.minHeight;
const max = isHorizontal ? constraints.maxWidth : constraints.maxHeight;
const atMax = max != null && value >= max - AT_CONSTRAINT_EPSILON;
const atMin = value <= min + AT_CONSTRAINT_EPSILON;
if (atMax && !atMin) {
return SHRINK_CURSOR[edge];
}
if (atMin && !atMax) {
return GROW_CURSOR[edge];
}
return isHorizontal ? 'ew-resize' : 'ns-resize';
};
const updateResizeHandleCursors = (container, size, constraints) => {
if (!container) {
return;
}
RESIZE_EDGE_HANDLES.forEach((edge) => {
const handle = container.querySelector(`.${RESIZE_HANDLE_CLASSES[edge]}`);
if (handle) {
handle.style.cursor = getEdgeResizeCursor(edge, size, constraints);
}
});
};
export const WindowModal = (props) => {
ensurePortalElement();
const positionDeltaRef = React.useRef(null);
const lastClampedRef = React.useRef(null);
const normalizedPosition = {
x: normalizeTopLeft(props.position?.x ?? 0, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET),
y: normalizeTopLeft(props.position?.y ?? 0, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET),
};
const positionRef = React.useRef(normalizedPosition);
const sizeRef = React.useRef(props.size);
const stacking = useStacking();
positionRef.current = normalizedPosition;
sizeRef.current = props.size;
const minWidth = props.minWidth ?? 0;
const minHeight = props.minHeight ?? 0;
const style = {
zIndex: stacking.zIndex,
position: 'absolute',
left: normalizedPosition.x,
top: normalizedPosition.y,
};
const handleDrop = (dx, dy) => {
const newPosition = {
x: positionRef.current.x + dx,
y: positionRef.current.y + dy,
};
const size = sizeRef.current;
const position = clampWindowModalPositionToViewport(newPosition, size, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
props.onChange({ position, size });
};
const { targetRef, applyTransform } = useDraggable({
handleSelector: props.handleSelector,
onDrop: handleDrop,
edgeOffset: WINDOW_MODAL_VIEWPORT_EDGE_OFFSET,
});
const constraintsRef = React.useRef({ minWidth, minHeight });
const getContentElement = () => {
const resizableEl = targetRef.current?.firstElementChild;
return resizableEl?.firstElementChild ?? null;
};
const computeEffectiveConstraints = () => {
const dom = readDomSizeConstraints(getContentElement());
return {
minWidth: Math.max(minWidth, dom.minWidth ?? 0),
minHeight: Math.max(minHeight, dom.minHeight ?? 0),
maxWidth: dom.maxWidth,
maxHeight: dom.maxHeight,
};
};
const handleResizeStart = () => {
constraintsRef.current = computeEffectiveConstraints();
updateResizeHandleCursors(targetRef.current, sizeRef.current, constraintsRef.current);
};
const handleResize = (_event, direction, elementRef, delta) => {
const constraints = constraintsRef.current;
const clamped = getClampedResizeState(direction, delta, positionRef.current, sizeRef.current, constraints.minWidth, constraints.minHeight, constraints.maxWidth, constraints.maxHeight);
lastClampedRef.current = clamped;
if (clamped.positionDelta) {
positionDeltaRef.current = clamped.positionDelta;
applyTransform(clamped.positionDelta.x, clamped.positionDelta.y);
}
elementRef.style.width = `${clamped.size.width}px`;
elementRef.style.height = `${clamped.size.height}px`;
updateResizeHandleCursors(targetRef.current, clamped.size, constraints);
if (isBrowserDocumentAvailable() &&
RESIZE_EDGE_HANDLES.includes(direction)) {
document.body.style.cursor = getEdgeResizeCursor(direction, clamped.size, constraints);
}
};
React.useEffect(() => {
updateResizeHandleCursors(targetRef.current, sizeRef.current, computeEffectiveConstraints());
}, [props.size.width, props.size.height, minWidth, minHeight]);
const handleResizeStop = () => {
applyTransform(0, 0);
positionDeltaRef.current = null;
if (isBrowserDocumentAvailable()) {
document.body.style.cursor = '';
}
const clamped = lastClampedRef.current ??
clampWindowModalToViewport(positionRef.current, sizeRef.current, minWidth, minHeight, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
lastClampedRef.current = null;
props.onChange(clamped);
};
const onChangeRef = React.useRef(props.onChange);
onChangeRef.current = props.onChange;
React.useEffect(() => {
if (!isBrowserDocumentAvailable()) {
return;
}
const handleViewportResize = () => {
const current = positionRef.current;
const currentSize = sizeRef.current;
const clamped = clampWindowModalToViewport(current, currentSize, minWidth, minHeight, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
if (clamped.position.x !== current.x ||
clamped.position.y !== current.y ||
clamped.size.width !== currentSize.width ||
clamped.size.height !== currentSize.height) {
onChangeRef.current(clamped);
}
};
window.addEventListener('resize', handleViewportResize);
return () => window.removeEventListener('resize', handleViewportResize);
}, [minWidth, minHeight]);
const ResizableCmp = Resizable;
const handleMouseDown = (event) => {
const node = targetRef.current;
if (node && event.target instanceof Node && !node.contains(event.target)) {
return;
}
stacking.bringInFront();
};
return createPortal(_jsx("div", { style: style, ref: targetRef, onMouseDown: handleMouseDown, children: _jsx(ResizableCmp, { minWidth: minWidth, minHeight: minHeight, onResizeStart: handleResizeStart, onResizeStop: handleResizeStop, onResize: handleResize, handleClasses: RESIZE_HANDLE_CLASSES, size: {
width: props.size.width,
height: props.size.height,
}, children: props.children }) }), portalElement);
};