UNPKG

@grafana/ui

Version:
312 lines (309 loc) • 11.2 kB
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