@grafana/ui
Version:
Grafana Components Library
312 lines (309 loc) • 11.2 kB
JavaScript
import { css } from '@emotion/css';
import { clamp } from 'lodash';
import { useRef, useCallback, useId } from 'react';
import { useStyles2 } from '../../themes/ThemeContext.mjs';
import { getDragStyles } from '../DragHandle/DragHandle.mjs';
const PIXELS_PER_MS = 0.3;
const VERTICAL_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown"]);
const HORIZONTAL_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight"]);
const propsForDirection = {
row: {
dim: "width",
axis: "clientX",
min: "minWidth",
max: "maxWidth"
},
column: {
dim: "height",
axis: "clientY",
min: "minHeight",
max: "maxHeight"
}
};
function useSplitter(options) {
const {
direction,
initialSize = options.usePixels ? 300 : 0.5,
dragPosition = "middle",
onResizing,
onSizeChanged,
usePixels
} = options;
const handleSize = getPixelSize(options.handleSize);
const splitterRef = useRef(null);
const firstPaneRef = useRef(null);
const secondPaneRef = useRef(null);
const containerRef = useRef(null);
const containerSize = useRef(null);
const primarySizeRef = useRef(null);
const referencePaneSize = useRef(void 0);
const savedPos = useRef(void 0);
const measurementProp = propsForDirection[direction].dim;
const clientAxis = propsForDirection[direction].axis;
const minDimProp = propsForDirection[direction].min;
const maxDimProp = propsForDirection[direction].max;
const dragStart = useRef(null);
const onPointerDown = useCallback(
(e) => {
if (!firstPaneRef.current || !secondPaneRef.current) {
return;
}
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
containerSize.current = containerRef.current.getBoundingClientRect()[measurementProp];
dragStart.current = e[clientAxis];
splitterRef.current.setPointerCapture(e.pointerId);
if (usePixels) {
referencePaneSize.current = measureElement(secondPaneRef.current);
} else {
referencePaneSize.current = measureElement(firstPaneRef.current);
}
savedPos.current = void 0;
},
[measurementProp, clientAxis, usePixels]
);
const onUpdateSize = useCallback(
(diff) => {
if (!containerSize.current || !primarySizeRef.current || !secondPaneRef.current) {
return;
}
const firstPanePixels = primarySizeRef.current;
const secondPanePixels = containerSize.current - firstPanePixels - handleSize;
const dims = referencePaneSize.current;
if (usePixels) {
const newSize = clamp(secondPanePixels - diff, dims[minDimProp], dims[maxDimProp]);
secondPaneRef.current.style.flexBasis = `${newSize}px`;
splitterRef.current.ariaValueNow = `${newSize}`;
onResizing == null ? void 0 : onResizing(newSize, firstPanePixels + diff, newSize);
} else {
const newSize = clamp(primarySizeRef.current + diff, dims[minDimProp], dims[maxDimProp]);
const newFlex = newSize / (containerSize.current - handleSize);
firstPaneRef.current.style.flexGrow = `${newFlex}`;
secondPaneRef.current.style.flexGrow = `${1 - newFlex}`;
splitterRef.current.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]);
onResizing == null ? void 0 : onResizing(newFlex, newSize, secondPanePixels - diff);
}
},
[onResizing, handleSize, usePixels, minDimProp, maxDimProp]
);
const onPointerMove = useCallback(
(e) => {
if (dragStart.current !== null) {
onUpdateSize(e[clientAxis] - dragStart.current);
}
},
[onUpdateSize, clientAxis]
);
const onPointerUp = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
dragStart.current = null;
splitterRef.current.releasePointerCapture(e.pointerId);
const firstPaneSize = firstPaneRef.current.getBoundingClientRect()[measurementProp];
const secondPanePixels = containerSize.current - firstPaneSize - handleSize;
onSizeChanged == null ? void 0 : onSizeChanged(parseFloat(firstPaneRef.current.style.flexGrow), firstPaneSize, secondPanePixels);
},
[onSizeChanged, handleSize, measurementProp]
);
const pressedKeys = useRef(/* @__PURE__ */ new Set());
const keysLastHandledAt = useRef(null);
const handlePressedKeys = useCallback(
(time) => {
var _a;
const nothingPressed = pressedKeys.current.size === 0;
if (nothingPressed) {
keysLastHandledAt.current = null;
return;
} else if (primarySizeRef.current === null) {
return;
}
const dt = time - ((_a = keysLastHandledAt.current) != null ? _a : time);
const dx = dt * PIXELS_PER_MS;
let sizeChange = 0;
if (direction === "row") {
if (pressedKeys.current.has("ArrowLeft")) {
sizeChange -= dx;
}
if (pressedKeys.current.has("ArrowRight")) {
sizeChange += dx;
}
} else {
if (pressedKeys.current.has("ArrowUp")) {
sizeChange -= dx;
}
if (pressedKeys.current.has("ArrowDown")) {
sizeChange += dx;
}
}
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
containerSize.current = containerRef.current.getBoundingClientRect()[measurementProp];
onUpdateSize(sizeChange);
keysLastHandledAt.current = time;
window.requestAnimationFrame(handlePressedKeys);
},
[direction, measurementProp, onUpdateSize]
);
const onKeyDown = useCallback(
(e) => {
if (!firstPaneRef.current || !secondPaneRef.current || !splitterRef.current || !containerRef.current) {
return;
}
if (!(direction === "column" && VERTICAL_KEYS.has(e.key) || direction === "row" && HORIZONTAL_KEYS.has(e.key)) || pressedKeys.current.has(e.key)) {
return;
}
savedPos.current = void 0;
e.preventDefault();
e.stopPropagation();
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
containerSize.current = containerRef.current.getBoundingClientRect()[measurementProp];
if (usePixels) {
referencePaneSize.current = measureElement(secondPaneRef.current);
} else {
referencePaneSize.current = measureElement(firstPaneRef.current);
}
const newKey = !pressedKeys.current.has(e.key);
if (newKey) {
const initiateAnimationLoop = pressedKeys.current.size === 0;
pressedKeys.current.add(e.key);
if (initiateAnimationLoop) {
window.requestAnimationFrame(handlePressedKeys);
}
}
},
[direction, handlePressedKeys, , measurementProp, usePixels]
);
const onKeyUp = useCallback(
(e) => {
if (direction === "row" && !HORIZONTAL_KEYS.has(e.key) || direction === "column" && !VERTICAL_KEYS.has(e.key)) {
return;
}
pressedKeys.current.delete(e.key);
if (primarySizeRef.current !== null) {
const secondPanePixels = containerSize.current - primarySizeRef.current - handleSize;
onSizeChanged == null ? void 0 : onSizeChanged(parseFloat(firstPaneRef.current.style.flexGrow), primarySizeRef.current, secondPanePixels);
}
},
[direction, onSizeChanged, handleSize]
);
const onDoubleClick = useCallback(() => {
if (!firstPaneRef.current || !secondPaneRef.current) {
return;
}
if (usePixels) {
secondPaneRef.current.style.flexBasis = `${initialSize}px`;
} else {
firstPaneRef.current.style.flexGrow = "0.5";
secondPaneRef.current.style.flexGrow = "0.5";
primarySizeRef.current = firstPaneRef.current.getBoundingClientRect()[measurementProp];
splitterRef.current.ariaValueNow = `50`;
}
}, [measurementProp, usePixels, initialSize]);
const onBlur = useCallback(() => {
if (pressedKeys.current.size > 0) {
pressedKeys.current.clear();
dragStart.current = null;
if (typeof primarySizeRef.current === "number") {
const secondPanePixels = containerSize.current - primarySizeRef.current - handleSize;
onSizeChanged == null ? void 0 : onSizeChanged(parseFloat(firstPaneRef.current.style.flexGrow), primarySizeRef.current, secondPanePixels);
}
}
}, [onSizeChanged, handleSize]);
const styles = useStyles2(getStyles, direction);
const dragStyles = useStyles2(getDragStyles, dragPosition);
const dragHandleStyle = direction === "column" ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical;
const id = useId();
const primaryStyles = {
flexGrow: clamp(initialSize, 0, 1),
[minDimProp]: "min-content"
};
const secondaryStyles = {
flexGrow: clamp(1 - initialSize, 0, 1),
[minDimProp]: "min-content"
};
if (usePixels) {
primaryStyles.flexGrow = 1;
secondaryStyles.flexGrow = "unset";
secondaryStyles.flexBasis = `${initialSize}px`;
}
const primaryId = `start-panel-${id}`;
return {
containerProps: {
ref: containerRef,
className: styles.container
},
primaryProps: {
ref: firstPaneRef,
className: styles.panel,
style: primaryStyles,
id: primaryId
},
secondaryProps: {
ref: secondPaneRef,
className: styles.panel,
style: secondaryStyles
},
splitterProps: {
onPointerUp,
onPointerDown,
onPointerMove,
onKeyDown,
onKeyUp,
onDoubleClick,
onBlur,
ref: splitterRef,
style: { [measurementProp]: `${handleSize}px` },
role: "separator",
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-valuenow": initialSize * 100,
"aria-controls": primaryId,
"aria-label": "Pane resize widget",
tabIndex: 0,
className: dragHandleStyle
}
};
}
function ariaValue(value, min, max) {
return `${clamp((value - min) / (max - min) * 100, 0, 100)}`;
}
function measureElement(ref, usePixels) {
const savedBodyOverflow = document.body.style.overflow;
const savedWidth = ref.style.width;
const savedHeight = ref.style.height;
const savedFlex = ref.style.flexGrow;
const savedFlexBasis = ref.style.flexBasis;
document.body.style.overflow = "hidden";
ref.style.flexGrow = "0";
ref.style.flexBasis = "0";
const { width: minWidth, height: minHeight } = ref.getBoundingClientRect();
ref.style.flexGrow = "100";
const { width: maxWidth, height: maxHeight } = ref.getBoundingClientRect();
document.body.style.overflow = savedBodyOverflow;
ref.style.width = savedWidth;
ref.style.height = savedHeight;
ref.style.flexGrow = savedFlex;
ref.style.flexBasis = savedFlexBasis;
return { minWidth, maxWidth, minHeight, maxHeight };
}
function getStyles(theme, direction) {
return {
container: css({
display: "flex",
flexDirection: direction === "row" ? "row" : "column",
width: "100%",
flexGrow: 1,
overflow: "hidden"
}),
panel: css({ display: "flex", position: "relative", flexBasis: 0 })
};
}
function getPixelSize(size = "md") {
return {
xs: 4,
sm: 8,
md: 16,
lg: 32
}[size];
}
export { useSplitter };
//# sourceMappingURL=useSplitter.mjs.map