@gfazioli/mantine-split-pane
Version:
A Mantine 9 React component for resizable split pane layouts with 7 resizer variants, context-based prop inheritance, responsive orientation, and dynamic pane generation.
307 lines (304 loc) • 10.8 kB
JavaScript
'use client';
import { createVarsResolver, factory, useProps, px, useStyles, Box } from '@mantine/core';
import React, { useRef, useImperativeHandle, useCallback, useEffect } from 'react';
import { useResponsiveValue } from '../hooks/use-responsive-value.mjs';
import { useSplitContext } from '../Split.context.mjs';
import classes from './SplitPane.module.css.mjs';
const varsResolver = createVarsResolver((_, { grow }) => {
return {
root: {
"--split-pane-grow": grow ? 1 : "initial"
}
};
});
const defaultProps = {
grow: false
};
function isPercentageValue(size) {
return typeof size === "string" && size.includes("%");
}
function withPx(value) {
if (typeof value === "number") {
return `${value}px`;
}
if (typeof value === "string" && value.includes("px")) {
return value;
}
return value;
}
const SplitPane = factory((_props) => {
const { ref, ...restProps } = _props;
const props = useProps("Pane", defaultProps, restProps);
const ctx = useSplitContext();
const {
children,
grow,
initialWidth: initialWidthProp,
initialHeight: initialHeightProp,
minWidth: minWidthProp,
minHeight: minHeightProp,
maxWidth: maxWidthProp,
maxHeight: maxHeightProp,
onResizeStart,
onResizing,
onResizeEnd,
onResetInitialSize,
className,
style,
classNames,
styles,
unstyled,
vars,
mod,
...others
} = props;
const initialWidth = useResponsiveValue(initialWidthProp);
const initialHeight = useResponsiveValue(initialHeightProp);
const minWidth = useResponsiveValue(minWidthProp);
const minHeight = useResponsiveValue(minHeightProp);
const maxWidth = useResponsiveValue(maxWidthProp);
const maxHeight = useResponsiveValue(maxHeightProp);
const localRef = useRef(null);
const hasBeenDraggedRef = useRef(false);
const dragRatioRef = useRef(null);
const prevContainerSizeRef = useRef(null);
useImperativeHandle(ref, () => ({
...localRef.current,
splitPane: localRef.current,
resetInitialSize,
getMinWidth: () => getSizeInPixel(minWidth),
getMinHeight: () => getSizeInPixel(minHeight),
getMaxWidth: () => getSizeInPixel(maxWidth),
getMaxHeight: () => getSizeInPixel(maxHeight),
getInitialWidth: () => initialWidth,
getInitialHeight: () => initialHeight,
onResizeStart: () => onResizeStart && onResizeStart(),
onResizing: (size) => onResizing && onResizing(size),
onResizeEnd: (size) => {
if (localRef.current && ctx.containerSize) {
const rect = localRef.current.getBoundingClientRect();
if (ctx.orientation === "vertical" && ctx.containerSize.width > 0) {
dragRatioRef.current = rect.width / ctx.containerSize.width;
hasBeenDraggedRef.current = true;
} else if (ctx.orientation === "horizontal" && ctx.containerSize.height > 0) {
dragRatioRef.current = rect.height / ctx.containerSize.height;
hasBeenDraggedRef.current = true;
}
}
onResizeEnd && onResizeEnd(size);
},
// Plain user-callback emitters used after a double-click reset: they
// forward to the consumer's `onResizing` / `onResizeEnd` props without
// touching `dragRatioRef` / `hasBeenDraggedRef`, so the reset that
// `resetInitialSize` just performed is not immediately undone.
notifyResizing: (size) => onResizing && onResizing(size),
notifyResizeEnd: (size) => onResizeEnd && onResizeEnd(size)
}));
const initialWidthRef = useRef(null);
const initialHeightRef = useRef(null);
const getSizeInPixel = useCallback(
(size) => {
if (size === void 0 || size === null) {
return void 0;
}
if (typeof size === "string" && size.includes("%")) {
const value = parseFloat(size);
const parentEl = localRef.current?.parentElement;
if (!parentEl) {
return void 0;
}
const dimension = ctx.orientation === "vertical" ? "width" : "height";
const parentSize = parentEl.getBoundingClientRect()[dimension];
return parentSize * value / 100;
}
const result = px(size);
return typeof result === "number" && !Number.isNaN(result) ? result : void 0;
},
[ctx.orientation]
);
const getInitialVerticalSize = useCallback(() => {
if (ctx.orientation === "vertical") {
const currentWidth = localRef.current?.getBoundingClientRect().width ?? 0;
if (!initialWidth && !minWidth) {
return currentWidth;
}
if (initialWidth && !minWidth) {
return getSizeInPixel(initialWidth);
}
if (!initialWidth && minWidth) {
return getSizeInPixel(minWidth);
}
if (initialWidth && minWidth) {
return Math.max(getSizeInPixel(initialWidth) ?? 0, getSizeInPixel(minWidth) ?? 0);
}
}
return "auto";
}, [ctx.orientation, initialWidth, minWidth, getSizeInPixel]);
const getInitialHorizontalSize = useCallback(() => {
if (ctx.orientation === "horizontal") {
const currentHeight = localRef.current?.getBoundingClientRect().height ?? 0;
if (!initialHeight && !minHeight) {
return currentHeight;
}
if (initialHeight && !minHeight) {
return getSizeInPixel(initialHeight);
}
if (!initialHeight && minHeight) {
return getSizeInPixel(minHeight);
}
if (initialHeight && minHeight) {
return Math.max(getSizeInPixel(initialHeight) ?? 0, getSizeInPixel(minHeight) ?? 0);
}
}
return "auto";
}, [ctx.orientation, initialHeight, minHeight, getSizeInPixel]);
useEffect(() => {
initialWidthRef.current = getInitialVerticalSize() ?? null;
initialHeightRef.current = getInitialHorizontalSize() ?? null;
if (localRef.current) {
localRef.current.style.width = withPx(getInitialVerticalSize() ?? "auto");
localRef.current.style.height = withPx(getInitialHorizontalSize() ?? "auto");
}
}, [getInitialVerticalSize, getInitialHorizontalSize]);
useEffect(() => {
hasBeenDraggedRef.current = false;
dragRatioRef.current = null;
const newWidth = getInitialVerticalSize();
const newHeight = getInitialHorizontalSize();
initialWidthRef.current = newWidth ?? null;
initialHeightRef.current = newHeight ?? null;
if (localRef.current) {
localRef.current.style.width = withPx(newWidth ?? "auto");
localRef.current.style.height = withPx(newHeight ?? "auto");
}
}, [getInitialVerticalSize, getInitialHorizontalSize]);
useEffect(() => {
if (!ctx.containerSize || !localRef.current) {
return;
}
const { width: containerWidth, height: containerHeight } = ctx.containerSize;
if (containerWidth === 0 && containerHeight === 0) {
return;
}
if (!prevContainerSizeRef.current) {
prevContainerSizeRef.current = { width: containerWidth, height: containerHeight };
return;
}
const prevSize = prevContainerSizeRef.current;
const widthChanged = Math.abs(containerWidth - prevSize.width) > 1;
const heightChanged = Math.abs(containerHeight - prevSize.height) > 1;
if (!widthChanged && !heightChanged) {
return;
}
prevContainerSizeRef.current = { width: containerWidth, height: containerHeight };
if (grow) {
return;
}
if (ctx.orientation === "vertical" && widthChanged) {
if (hasBeenDraggedRef.current && dragRatioRef.current !== null) {
const minPx = getSizeInPixel(minWidth);
const maxPx = getSizeInPixel(maxWidth);
let newWidth = dragRatioRef.current * containerWidth;
if (minPx !== void 0) {
newWidth = Math.max(newWidth, minPx);
}
if (maxPx !== void 0) {
newWidth = Math.min(newWidth, maxPx);
}
localRef.current.style.width = `${newWidth}px`;
} else if (isPercentageValue(initialWidth)) {
const newWidth = getSizeInPixel(initialWidth);
if (newWidth !== void 0) {
const minPx = getSizeInPixel(minWidth);
const maxPx = getSizeInPixel(maxWidth);
let finalWidth = newWidth;
if (minPx !== void 0) {
finalWidth = Math.max(finalWidth, minPx);
}
if (maxPx !== void 0) {
finalWidth = Math.min(finalWidth, maxPx);
}
localRef.current.style.width = `${finalWidth}px`;
}
}
}
if (ctx.orientation === "horizontal" && heightChanged) {
if (hasBeenDraggedRef.current && dragRatioRef.current !== null) {
const minPx = getSizeInPixel(minHeight);
const maxPx = getSizeInPixel(maxHeight);
let newHeight = dragRatioRef.current * containerHeight;
if (minPx !== void 0) {
newHeight = Math.max(newHeight, minPx);
}
if (maxPx !== void 0) {
newHeight = Math.min(newHeight, maxPx);
}
localRef.current.style.height = `${newHeight}px`;
} else if (isPercentageValue(initialHeight)) {
const newHeight = getSizeInPixel(initialHeight);
if (newHeight !== void 0) {
const minPx = getSizeInPixel(minHeight);
const maxPx = getSizeInPixel(maxHeight);
let finalHeight = newHeight;
if (minPx !== void 0) {
finalHeight = Math.max(finalHeight, minPx);
}
if (maxPx !== void 0) {
finalHeight = Math.min(finalHeight, maxPx);
}
localRef.current.style.height = `${finalHeight}px`;
}
}
}
}, [
ctx.containerSize,
ctx.orientation,
grow,
initialWidth,
initialHeight,
minWidth,
minHeight,
maxWidth,
maxHeight,
getSizeInPixel
]);
const getStyles = useStyles({
name: "SplitPane",
classes,
props,
className,
style,
classNames,
styles,
unstyled,
vars,
varsResolver
});
const resetInitialSize = (e) => {
e.preventDefault();
e.stopPropagation();
if (!localRef.current) {
return;
}
hasBeenDraggedRef.current = false;
dragRatioRef.current = null;
if (ctx.orientation === "vertical") {
const newSize = getInitialVerticalSize();
initialWidthRef.current = newSize ?? null;
localRef.current.style.width = withPx(newSize ?? "auto");
}
if (ctx.orientation === "horizontal") {
const newSize = getInitialHorizontalSize();
initialHeightRef.current = newSize ?? null;
localRef.current.style.height = withPx(newSize ?? "auto");
}
if (onResetInitialSize) {
onResetInitialSize(e);
}
};
return /* @__PURE__ */ React.createElement(Box, { ref: localRef, mod: { orientation: ctx.orientation }, ...others, ...getStyles("root") }, children);
});
SplitPane.classes = classes;
SplitPane.displayName = "SplitPane";
export { SplitPane };
//# sourceMappingURL=SplitPane.mjs.map