Skip to content

Instantly share code, notes, and snippets.

@jakobvase
Last active May 11, 2023 14:48
Show Gist options
  • Save jakobvase/6fbd43b838d38f55d7d713d76e4e0a75 to your computer and use it in GitHub Desktop.
Save jakobvase/6fbd43b838d38f55d7d713d76e4e0a75 to your computer and use it in GitHub Desktop.
useSplitPanel
import {
MouseEvent as ReactMouseEvent,
RefObject,
useCallback,
useEffect,
useRef,
} from "react";
const rootFontSize = 13;
const calculateProportion = ({
containerWidth,
containerXOffset,
lineWidth,
firstChildMinWidth,
lastChildMinWidth,
mouseX,
mouseOnLineOffsetX,
}: {
containerWidth: number;
containerXOffset: number;
lineWidth: number;
firstChildMinWidth: number;
lastChildMinWidth: number;
mouseX: number;
mouseOnLineOffsetX: number;
}): number | null => {
const leftoverSpace =
containerWidth -
(lineWidth +
firstChildMinWidth * rootFontSize +
lastChildMinWidth * rootFontSize);
const relX =
mouseX -
containerXOffset -
mouseOnLineOffsetX -
firstChildMinWidth * rootFontSize;
if (leftoverSpace <= 0) {
return null;
}
const relativeWidth = relX / leftoverSpace;
const newSplitWidth =
relativeWidth < 0.01 ? 0 : relativeWidth > 0.99 ? 1 : relativeWidth;
return newSplitWidth;
};
/**
* Options for useSplitPanel.
*/
type Options = {
/** An initial proportion. A number between 0 and 1. Defaults to 0.5. */
initialProportion?: number;
/** A reference to the line between the two panels. */
lineRef: RefObject<HTMLElement>;
/** A reference to the container holding the panels and the line. */
containerRef: RefObject<HTMLElement>;
/** Min width in rem of the first child. Defaults to 20. */
firstChildMinWidth?: number;
/** Min width in rem of the last child. Defaults to 20. */
lastChildMinWidth?: number;
/** Callback that happens when the user lets go of the mouse. */
onChange?: (newProportion: number) => void;
};
type Return = {
/** Attach this to onMouseDown on the line between the panels. */
onMouseDownLine: (e: ReactMouseEvent<HTMLElement, MouseEvent>) => void;
};
/**
* Hook for handling split panels that can resize.
*/
export const useSplitPanel = ({
lineRef,
containerRef,
firstChildMinWidth = 20,
lastChildMinWidth = 20,
initialProportion = 0.5,
onChange,
}: Options): Return => {
// Hook state. Use refs to avoid rerendering when the size changes.
const lineOffset = useRef(0);
const lastProportion = useRef(initialProportion);
const dragging = useRef(false);
// Initial setup.
useEffect(() => {
const container = containerRef.current;
if (container) {
container.style.display = "flex";
container.style.flexDirection = "row";
const firstChild = container.firstChild as HTMLElement;
const lastChild = container.lastChild as HTMLElement;
firstChild.style.flexGrow = initialProportion + "";
firstChild.style.flexShrink = "0";
firstChild.style.flexBasis = firstChildMinWidth + "rem";
lastChild.style.flexGrow = 1 - initialProportion + "";
lastChild.style.flexShrink = "0";
lastChild.style.flexBasis = lastChildMinWidth + "rem";
}
}, [containerRef, firstChildMinWidth, initialProportion, lastChildMinWidth]);
// Function to be attached to when the mouse moves.
const handleDrag = useCallback(
(e: MouseEvent) => {
e.preventDefault();
const container = containerRef.current;
const line = lineRef.current;
if (container && line && e.movementX !== 0) {
const { width: containerWidth, x: containerXOffset } =
container.getBoundingClientRect();
const { width: lineWidth } = line.getBoundingClientRect();
const newSplitWidth = calculateProportion({
containerWidth,
containerXOffset,
lineWidth,
firstChildMinWidth,
lastChildMinWidth,
mouseX: e.pageX,
mouseOnLineOffsetX: lineOffset.current,
});
if (newSplitWidth !== null) {
lastProportion.current = newSplitWidth;
(container.firstChild as HTMLDivElement).style.flexGrow =
newSplitWidth + "";
(container.lastChild as HTMLDivElement).style.flexGrow =
1 - newSplitWidth + "";
}
}
},
[containerRef, firstChildMinWidth, lastChildMinWidth, lineRef]
);
// Listener that removes the move-listener again.
useEffect(() => {
const listener = () => {
if (dragging.current) {
dragging.current = false;
window.removeEventListener("mousemove", handleDrag);
if (onChange) {
onChange(lastProportion.current);
}
}
};
window.addEventListener("mouseup", listener);
return () => {
window.removeEventListener("mouseup", listener);
};
}, [handleDrag, onChange]);
return {
onMouseDownLine: useCallback(
(e) => {
dragging.current = true;
e.preventDefault();
lineOffset.current =
e.pageX - (lineRef.current?.getBoundingClientRect().x ?? e.pageX);
window.addEventListener("mousemove", handleDrag);
},
[handleDrag, lineRef]
),
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment