@gfazioli/mantine-split-pane
Version:
A React component that manages split panes allows users to divide and resize content areas within a layout efficiently.
444 lines (441 loc) • 15.8 kB
JavaScript
'use client';
import React, { useRef } from 'react';
import { createVarsResolver, getSize, getRadius, parseThemeColor, getGradient, factory, useProps, useStyles, UnstyledButton } from '@mantine/core';
import { useSplitContext } from '../Split.context.mjs';
import classes from './SplitPaneResizer.module.css.mjs';
const varsResolver = createVarsResolver(
(theme, {
size,
opacity,
radius,
color,
hoverColor,
knobSize,
knobOpacity,
knobRadius,
knobColor,
knobHoverColor,
withKnob,
knobAlwaysOn,
spacing,
variant,
cursorVertical,
cursorHorizontal,
gradient,
hoverGradient
}) => {
const knobVariant = variant === "dotted" || variant === "dashed";
const forceKnobOpacityValue = withKnob && knobAlwaysOn && !knobVariant ? knobOpacity : "0";
const colors = variantColorResolver({
color,
hover: hoverColor,
knob: knobColor,
hoverKnob: knobHoverColor,
theme,
gradient,
hoverGradient,
variant: variant || "filled"
});
return {
root: {
"--split-resizer-size": getSize(size, "split-resizer-size"),
"--split-resizer-color": colors.color,
"--split-resizer-hover-color": colors.hover,
"--split-resizer-radius": getRadius(radius),
"--split-resizer-opacity": opacity !== void 0 ? opacity : "1",
"--split-resizer-knob-size": getSize(knobSize, "split-resizer-knob-size"),
"--split-resizer-knob-opacity": forceKnobOpacityValue,
"--split-resizer-knob-hover-opacity": withKnob || knobVariant ? "1" : "0",
"--split-resizer-knob-radius": getRadius(knobRadius),
"--split-resizer-knob-color": colors.knob,
"--split-resizer-knob-hover-color": colors.hoverKnob,
"--split-resizer-spacing": getSize(spacing, "split-resizer-spacing"),
"--split-resizer-cursor-vertical": cursorVertical || "col-resize",
"--split-resizer-cursor-horizontal": cursorHorizontal || "row-resize"
}
};
}
);
const variantColorResolver = ({
color,
hover,
knob,
hoverKnob,
theme,
variant,
gradient,
hoverGradient
}) => {
const parsedColor = color ? parseThemeColor({ color, theme }).value : void 0;
const parsedHover = hover ? parseThemeColor({ color: hover, theme }).value : void 0;
const parsedKnob = knob ? parseThemeColor({ color: knob, theme }).value : void 0;
const parsedHoverKnob = hoverKnob ? parseThemeColor({ color: hoverKnob, theme }).value : void 0;
const colors = {
color: parsedColor,
hover: parsedHover,
knob: parsedKnob,
hoverKnob: parsedHoverKnob
};
if (variant === "gradient") {
colors.color = getGradient(gradient, theme);
colors.hover = getGradient(hoverGradient || gradient, theme);
}
return colors;
};
const defaultProps = {
orientation: "vertical",
opacity: 0.8,
size: "sm",
radius: "xs",
withKnob: false,
knobAlwaysOn: true,
knobSize: "sm",
knobOpacity: 0.5,
knobRadius: "sm",
knobColor: "white",
knobHoverColor: "white",
spacing: "xs",
step: 8,
shiftStep: 64,
cursorVertical: "col-resize",
cursorHorizontal: "row-resize"
};
const SplitPaneResizer = factory((_props, _) => {
const ctx = useSplitContext();
const props = useProps("SplitPaneResizer", { ...defaultProps, ...ctx }, _props);
const {
orientation,
opacity,
size,
radius,
withKnob,
knobAlwaysOn,
knobSize,
knobOpacity,
knobRadius,
knobColor,
knobHoverColor,
spacing,
step,
shiftStep,
cursorVertical,
cursorHorizontal,
color,
hoverColor,
variant,
gradient,
hoverGradient,
onResizeStart,
onResizing,
onResizeEnd,
onDoubleClick,
__beforeRef: beforeRef,
__afterRef: afterRef,
className,
style,
classNames,
styles,
unstyled,
vars,
mod,
...rest
} = props;
const getStyles = useStyles({
name: "SplitPaneResizer",
classes,
props,
className,
style,
classNames,
styles,
unstyled,
vars,
varsResolver
});
const containerRef = useRef(null);
const processVerticalSize = (deltaX = 0) => {
const minBeforeWidth = beforeRef.current.getMinWidth();
const maxBeforeWidth = beforeRef.current.getMaxWidth();
const minAfterWidth = afterRef.current.getMinWidth();
const maxAfterWidth = afterRef.current.getMaxWidth();
const beforePane = beforeRef.current.splitPane;
const afterPane = afterRef.current.splitPane;
let beforeWidth = beforePane.getBoundingClientRect().width;
let afterWidth = afterPane.getBoundingClientRect().width;
const isBeforeWidthMaxExceeded = maxBeforeWidth && beforeWidth + deltaX > maxBeforeWidth;
const isAfterWidthMaxExceeded = maxAfterWidth && afterWidth - deltaX > maxAfterWidth;
const isBeforeWidthMinExceeded = minBeforeWidth && beforeWidth + deltaX < minBeforeWidth;
const isAfterWidthMinExceeded = minAfterWidth && afterWidth - deltaX < minAfterWidth;
const isBeforeWidthNegative = beforeWidth + deltaX < 0;
const isAfterWidthNegative = afterWidth - deltaX < 0;
function setVerticalSize() {
const beforeWidthString = `${beforeWidth}px`;
const afterWidthString = `${afterWidth}px`;
const beforePaneSizes = {
width: beforeWidth,
height: beforePane.getBoundingClientRect().height
};
const afterPaneSizes = {
width: afterWidth,
height: afterPane.getBoundingClientRect().height
};
beforeRef.current.onResizing?.(beforePaneSizes);
afterRef.current.onResizing?.(afterPaneSizes);
onResizing?.({
beforePane: beforePaneSizes,
afterPane: afterPaneSizes
});
beforePane.style.width = beforeWidthString;
afterPane.style.width = afterWidthString;
}
if (!isAfterWidthMaxExceeded && isBeforeWidthMinExceeded) {
afterWidth += beforeWidth - minBeforeWidth;
beforeWidth = minBeforeWidth;
return setVerticalSize();
}
if (!isAfterWidthMaxExceeded && isBeforeWidthNegative) {
afterWidth += beforeWidth;
beforeWidth = 0;
return setVerticalSize();
}
if (!isAfterWidthMinExceeded && !isAfterWidthNegative && isBeforeWidthMaxExceeded) {
afterWidth -= maxBeforeWidth - beforeWidth;
beforeWidth = maxBeforeWidth;
return setVerticalSize();
}
if (!isBeforeWidthMaxExceeded && isAfterWidthMinExceeded) {
beforeWidth += afterWidth - minAfterWidth;
afterWidth = minAfterWidth;
return setVerticalSize();
}
if (!isBeforeWidthMaxExceeded && isAfterWidthNegative) {
beforeWidth += afterWidth;
afterWidth = 0;
return setVerticalSize();
}
if (!isBeforeWidthMinExceeded && !isBeforeWidthNegative && isAfterWidthMaxExceeded) {
beforeWidth -= maxAfterWidth - afterWidth;
afterWidth = maxAfterWidth;
return setVerticalSize();
}
if (isBeforeWidthNegative || isAfterWidthNegative || isBeforeWidthMaxExceeded || isAfterWidthMaxExceeded || isBeforeWidthMinExceeded || isAfterWidthMinExceeded) {
return;
}
beforeWidth += deltaX;
afterWidth -= deltaX;
setVerticalSize();
};
const processHorizontalSize = (deltaY = 0) => {
const minBeforeHeight = beforeRef.current.getMinHeight();
const maxBeforeHeight = beforeRef.current.getMaxHeight();
const minAfterHeight = afterRef.current.getMinHeight();
const maxAfterHeight = afterRef.current.getMaxHeight();
const beforePane = beforeRef.current.splitPane;
const afterPane = afterRef.current.splitPane;
let beforeHeight = beforePane.getBoundingClientRect().height;
let afterHeight = afterPane.getBoundingClientRect().height;
const isBeforeHeightMaxExceeded = maxBeforeHeight && beforeHeight + deltaY > maxBeforeHeight;
const isAfterHeightMaxExceeded = maxAfterHeight && afterHeight - deltaY > maxAfterHeight;
const isBeforeHeightMinExceeded = minBeforeHeight && beforeHeight + deltaY < minBeforeHeight;
const isAfterHeightMinExceeded = minAfterHeight && afterHeight - deltaY < minAfterHeight;
const isBeforeHeightNegative = beforeHeight + deltaY < 0;
const isAfterHeightNegative = afterHeight - deltaY < 0;
function setHorizontalSize() {
const beforeHeightString = `${beforeHeight}px`;
const afterHeightString = `${afterHeight}px`;
const beforePaneSizes = {
width: beforePane.getBoundingClientRect().width,
height: beforeHeight
};
const afterPaneSizes = {
width: afterPane.getBoundingClientRect().width,
height: afterHeight
};
onResizing?.({
beforePane: beforePaneSizes,
afterPane: afterPaneSizes
});
beforeRef.current.onResizing?.(beforePaneSizes);
afterRef.current.onResizing?.(afterPaneSizes);
beforePane.style.height = beforeHeightString;
afterPane.style.height = afterHeightString;
}
if (!isAfterHeightMaxExceeded && isBeforeHeightMinExceeded) {
afterHeight += beforeHeight - minBeforeHeight;
beforeHeight = minBeforeHeight;
return setHorizontalSize();
}
if (!isAfterHeightMaxExceeded && isBeforeHeightNegative) {
afterHeight += beforeHeight;
beforeHeight = 0;
return setHorizontalSize();
}
if (!isAfterHeightMinExceeded && !isAfterHeightNegative && isBeforeHeightMaxExceeded) {
afterHeight -= maxBeforeHeight - beforeHeight;
beforeHeight = maxBeforeHeight;
return setHorizontalSize();
}
if (!isBeforeHeightMaxExceeded && isAfterHeightMinExceeded) {
beforeHeight += afterHeight - minAfterHeight;
afterHeight = minAfterHeight;
return setHorizontalSize();
}
if (!isBeforeHeightMaxExceeded && isAfterHeightNegative) {
beforeHeight += afterHeight;
afterHeight = 0;
return setHorizontalSize();
}
if (!isBeforeHeightMinExceeded && !isBeforeHeightNegative && isAfterHeightMaxExceeded) {
beforeHeight -= maxAfterHeight - afterHeight;
afterHeight = maxAfterHeight;
return setHorizontalSize();
}
if (isBeforeHeightNegative || isAfterHeightNegative || isBeforeHeightMaxExceeded || isAfterHeightMaxExceeded || isBeforeHeightMinExceeded || isAfterHeightMinExceeded) {
return;
}
beforeHeight += deltaY;
afterHeight -= deltaY;
setHorizontalSize();
};
const handleStart = (event) => {
event.preventDefault();
event.stopPropagation();
document.body.style.userSelect = "none";
document.body.style.webkitUserSelect = "none";
if (event.type === "mousedown") {
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleMouseUp);
}
if (event.type === "touchstart") {
document.addEventListener("touchmove", handleMove);
document.addEventListener("touchend", handleTouchEnd);
}
onResizeStart?.();
beforeRef.current.onResizeStart?.();
afterRef.current.onResizeStart?.();
document.body.style.cursor = orientation === "vertical" ? cursorVertical : cursorHorizontal;
};
const handleMove = (event) => {
if (!beforeRef.current || !afterRef.current) {
throw new Error("beforeRef or afterRef is not defined");
}
event.preventDefault();
event.stopPropagation();
const computedStyle = window.getComputedStyle(containerRef.current);
if (orientation === "vertical") {
const size2 = parseFloat(computedStyle.getPropertyValue("width"));
const clientX = "clientX" in event ? event.clientX : event.touches[0].clientX;
const deltaX = clientX - containerRef.current.getBoundingClientRect().left - size2 / 2;
return processVerticalSize(deltaX);
}
if (orientation === "horizontal") {
const size2 = parseFloat(computedStyle.getPropertyValue("height"));
const clientY = "clientY" in event ? event.clientY : event.touches[0].clientY;
const deltaY = clientY - containerRef.current.getBoundingClientRect().top - size2 / 2;
return processHorizontalSize(deltaY);
}
};
const handleMouseUp = () => {
if (!beforeRef.current || !afterRef.current) {
throw new Error("beforeRef or afterRef is not defined");
}
document.body.style.userSelect = "initial";
document.body.style.webkitUserSelect = "initial";
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "initial";
const beforePane = beforeRef.current.splitPane;
const afterPane = afterRef.current.splitPane;
const beforePaneSizes = {
width: beforePane.getBoundingClientRect().width,
height: beforePane.getBoundingClientRect().height
};
const afterPaneSizes = {
width: afterPane.getBoundingClientRect().width,
height: afterPane.getBoundingClientRect().height
};
onResizeEnd?.({
beforePane: beforePaneSizes,
afterPane: afterPaneSizes
});
beforeRef.current.onResizeEnd?.(beforePaneSizes);
afterRef.current.onResizeEnd?.(afterPaneSizes);
};
const handleTouchEnd = () => {
if (!beforeRef.current || !afterRef.current) {
throw new Error("beforeRef or afterRef is not defined");
}
document.removeEventListener("touchmove", handleMove);
document.removeEventListener("touchend", handleTouchEnd);
document.body.style.cursor = "initial";
const beforePane = beforeRef.current.splitPane;
const afterPane = afterRef.current.splitPane;
const beforePaneSizes = {
width: beforePane.getBoundingClientRect().width,
height: beforePane.getBoundingClientRect().height
};
const afterPaneSizes = {
width: afterPane.getBoundingClientRect().width,
height: afterPane.getBoundingClientRect().height
};
onResizeEnd?.({
beforePane: beforePaneSizes,
afterPane: afterPaneSizes
});
beforeRef.current.onResizeEnd?.(beforePaneSizes);
afterRef.current.onResizeEnd?.(afterPaneSizes);
};
const handleKeyUp = (event) => {
if (containerRef.current !== document.activeElement) {
return;
}
const code = event.nativeEvent.code;
const arrowLeftRight = code === "ArrowRight" || code === "ArrowLeft";
const arrowUpDown = code === "ArrowUp" || code === "ArrowDown";
const delta = event.shiftKey ? shiftStep : step;
if (orientation === "vertical" && arrowLeftRight) {
event.preventDefault();
event.stopPropagation();
const deltaSign = code === "ArrowRight" ? 1 : -1;
const deltaX = delta * deltaSign;
return processVerticalSize(deltaX);
}
if (orientation === "horizontal" && arrowUpDown) {
event.preventDefault();
event.stopPropagation();
const deltaSign = code === "ArrowDown" ? 1 : -1;
const deltaY = delta * deltaSign;
return processHorizontalSize(deltaY);
}
if (code === "Escape") {
event.preventDefault();
event.stopPropagation();
containerRef.current.blur();
}
};
const handleDoubleClick = (e) => {
e.preventDefault();
e.stopPropagation();
beforeRef.current.resetInitialSize(e);
afterRef.current.resetInitialSize(e);
onDoubleClick?.(e);
};
return /* @__PURE__ */ React.createElement(
UnstyledButton,
{
ref: containerRef,
mod: { orientation },
onMouseDown: handleStart,
onKeyDown: handleKeyUp,
onTouchStart: handleStart,
onDoubleClick: handleDoubleClick,
"aria-label": "Resize",
...getStyles("root", { variant: variant || "default" }),
...rest
}
);
});
SplitPaneResizer.classes = classes;
SplitPaneResizer.displayName = "SplitPaneResizer";
export { SplitPaneResizer, defaultProps };
//# sourceMappingURL=SplitPaneResizer.mjs.map