UNPKG

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