UNPKG

ivt

Version:

Ivt Components Library

1,206 lines (1,204 loc) 92.8 kB
import * as React from 'react'; import { forwardRef, createElement, useRef, useEffect, useContext, createContext, useState, useLayoutEffect, useImperativeHandle, useCallback, useMemo } from 'react'; import { c as cn } from '../chunks/utils-05LlW3Cl.mjs'; import { c as createLucideIcon } from '../chunks/createLucideIcon-DLrNgMqk.mjs'; import '../chunks/bundle-mjs-BYcyWisL.mjs'; const __iconNode = [ [ "circle", { cx: "9", cy: "12", r: "1", key: "1vctgf" } ], [ "circle", { cx: "9", cy: "5", r: "1", key: "hp0tcf" } ], [ "circle", { cx: "9", cy: "19", r: "1", key: "fkjjf6" } ], [ "circle", { cx: "15", cy: "12", r: "1", key: "1tmaij" } ], [ "circle", { cx: "15", cy: "5", r: "1", key: "19l28e" } ], [ "circle", { cx: "15", cy: "19", r: "1", key: "f4zoj3" } ] ]; const GripVertical = createLucideIcon("GripVertical", __iconNode); const isBrowser = typeof window !== "undefined"; // The "contextmenu" event is not supported as a PointerEvent in all browsers yet, so MouseEvent still need to be handled const PanelGroupContext = createContext(null); PanelGroupContext.displayName = "PanelGroupContext"; const DATA_ATTRIBUTES = { group: "data-panel-group", groupDirection: "data-panel-group-direction", groupId: "data-panel-group-id", panel: "data-panel", panelCollapsible: "data-panel-collapsible", panelId: "data-panel-id", panelSize: "data-panel-size", resizeHandle: "data-resize-handle", resizeHandleActive: "data-resize-handle-active", resizeHandleEnabled: "data-panel-resize-handle-enabled", resizeHandleId: "data-panel-resize-handle-id", resizeHandleState: "data-resize-handle-state" }; const PRECISION = 10; const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : ()=>{}; const useId = React["useId".toString()]; const wrappedUseId = typeof useId === "function" ? useId : ()=>null; let counter = 0; function useUniqueId(idFromParams = null) { const idFromUseId = wrappedUseId(); const idRef = useRef(idFromParams || idFromUseId || null); if (idRef.current === null) { idRef.current = "" + counter++; } return idFromParams !== null && idFromParams !== void 0 ? idFromParams : idRef.current; } function PanelWithForwardedRef({ children, className: classNameFromProps = "", collapsedSize, collapsible, defaultSize, forwardedRef, id: idFromProps, maxSize, minSize, onCollapse, onExpand, onResize, order, style: styleFromProps, tagName: Type = "div", ...rest }) { const context = useContext(PanelGroupContext); if (context === null) { throw Error(`Panel components must be rendered within a PanelGroup container`); } const { collapsePanel, expandPanel, getPanelSize, getPanelStyle, groupId, isPanelCollapsed, reevaluatePanelConstraints, registerPanel, resizePanel, unregisterPanel } = context; const panelId = useUniqueId(idFromProps); const panelDataRef = useRef({ callbacks: { onCollapse, onExpand, onResize }, constraints: { collapsedSize, collapsible, defaultSize, maxSize, minSize }, id: panelId, idIsFromProps: idFromProps !== undefined, order }); useRef({ didLogMissingDefaultSizeWarning: false }); useIsomorphicLayoutEffect(()=>{ const { callbacks, constraints } = panelDataRef.current; const prevConstraints = { ...constraints }; panelDataRef.current.id = panelId; panelDataRef.current.idIsFromProps = idFromProps !== undefined; panelDataRef.current.order = order; callbacks.onCollapse = onCollapse; callbacks.onExpand = onExpand; callbacks.onResize = onResize; constraints.collapsedSize = collapsedSize; constraints.collapsible = collapsible; constraints.defaultSize = defaultSize; constraints.maxSize = maxSize; constraints.minSize = minSize; // If constraints have changed, we should revisit panel sizes. // This is uncommon but may happen if people are trying to implement pixel based constraints. if (prevConstraints.collapsedSize !== constraints.collapsedSize || prevConstraints.collapsible !== constraints.collapsible || prevConstraints.maxSize !== constraints.maxSize || prevConstraints.minSize !== constraints.minSize) { reevaluatePanelConstraints(panelDataRef.current, prevConstraints); } }); useIsomorphicLayoutEffect(()=>{ const panelData = panelDataRef.current; registerPanel(panelData); return ()=>{ unregisterPanel(panelData); }; }, [ order, panelId, registerPanel, unregisterPanel ]); useImperativeHandle(forwardedRef, ()=>({ collapse: ()=>{ collapsePanel(panelDataRef.current); }, expand: (minSize)=>{ expandPanel(panelDataRef.current, minSize); }, getId () { return panelId; }, getSize () { return getPanelSize(panelDataRef.current); }, isCollapsed () { return isPanelCollapsed(panelDataRef.current); }, isExpanded () { return !isPanelCollapsed(panelDataRef.current); }, resize: (size)=>{ resizePanel(panelDataRef.current, size); } }), [ collapsePanel, expandPanel, getPanelSize, isPanelCollapsed, panelId, resizePanel ]); const style = getPanelStyle(panelDataRef.current, defaultSize); return createElement(Type, { ...rest, children, className: classNameFromProps, id: panelId, style: { ...style, ...styleFromProps }, // CSS selectors [DATA_ATTRIBUTES.groupId]: groupId, [DATA_ATTRIBUTES.panel]: "", [DATA_ATTRIBUTES.panelCollapsible]: collapsible || undefined, [DATA_ATTRIBUTES.panelId]: panelId, [DATA_ATTRIBUTES.panelSize]: parseFloat("" + style.flexGrow).toFixed(1) }); } const Panel = forwardRef((props, ref)=>createElement(PanelWithForwardedRef, { ...props, forwardedRef: ref })); PanelWithForwardedRef.displayName = "Panel"; Panel.displayName = "forwardRef(Panel)"; let currentCursorStyle = null; let prevRuleIndex = -1; let styleElement = null; function getCursorStyle(state, constraintFlags, isPointerDown) { const horizontalMin = (constraintFlags & EXCEEDED_HORIZONTAL_MIN) !== 0; const horizontalMax = (constraintFlags & EXCEEDED_HORIZONTAL_MAX) !== 0; const verticalMin = (constraintFlags & EXCEEDED_VERTICAL_MIN) !== 0; const verticalMax = (constraintFlags & EXCEEDED_VERTICAL_MAX) !== 0; if (constraintFlags) { if (horizontalMin) { if (verticalMin) { return "se-resize"; } else if (verticalMax) { return "ne-resize"; } else { return "e-resize"; } } else if (horizontalMax) { if (verticalMin) { return "sw-resize"; } else if (verticalMax) { return "nw-resize"; } else { return "w-resize"; } } else if (verticalMin) { return "s-resize"; } else if (verticalMax) { return "n-resize"; } } switch(state){ case "horizontal": return "ew-resize"; case "intersection": return "move"; case "vertical": return "ns-resize"; } } function resetGlobalCursorStyle() { if (styleElement !== null) { document.head.removeChild(styleElement); currentCursorStyle = null; styleElement = null; prevRuleIndex = -1; } } function setGlobalCursorStyle(state, constraintFlags, isPointerDown) { var _styleElement$sheet$i, _styleElement$sheet2; const style = getCursorStyle(state, constraintFlags); if (currentCursorStyle === style) { return; } currentCursorStyle = style; if (styleElement === null) { styleElement = document.createElement("style"); document.head.appendChild(styleElement); } if (prevRuleIndex >= 0) { var _styleElement$sheet; (_styleElement$sheet = styleElement.sheet) === null || _styleElement$sheet === void 0 ? void 0 : _styleElement$sheet.removeRule(prevRuleIndex); } prevRuleIndex = (_styleElement$sheet$i = (_styleElement$sheet2 = styleElement.sheet) === null || _styleElement$sheet2 === void 0 ? void 0 : _styleElement$sheet2.insertRule(`*{cursor: ${style} !important;}`)) !== null && _styleElement$sheet$i !== void 0 ? _styleElement$sheet$i : -1; } function isKeyDown(event) { return event.type === "keydown"; } function isPointerEvent(event) { return event.type.startsWith("pointer"); } function isMouseEvent(event) { return event.type.startsWith("mouse"); } function getResizeEventCoordinates(event) { if (isPointerEvent(event)) { if (event.isPrimary) { return { x: event.clientX, y: event.clientY }; } } else if (isMouseEvent(event)) { return { x: event.clientX, y: event.clientY }; } return { x: Infinity, y: Infinity }; } function getInputType() { if (typeof matchMedia === "function") { return matchMedia("(pointer:coarse)").matches ? "coarse" : "fine"; } } function intersects(rectOne, rectTwo, strict) { { return rectOne.x < rectTwo.x + rectTwo.width && rectOne.x + rectOne.width > rectTwo.x && rectOne.y < rectTwo.y + rectTwo.height && rectOne.y + rectOne.height > rectTwo.y; } } // Forked from NPM stacking-order@2.0.0 /** * Determine which of two nodes appears in front of the other — * if `a` is in front, returns 1, otherwise returns -1 * @param {HTMLElement | SVGElement} a * @param {HTMLElement | SVGElement} b */ function compare(a, b) { if (a === b) throw new Error("Cannot compare node with itself"); const ancestors = { a: get_ancestors(a), b: get_ancestors(b) }; let common_ancestor; // remove shared ancestors while(ancestors.a.at(-1) === ancestors.b.at(-1)){ a = ancestors.a.pop(); b = ancestors.b.pop(); common_ancestor = a; } assert(common_ancestor, "Stacking order can only be calculated for elements with a common ancestor"); const z_indexes = { a: get_z_index(find_stacking_context(ancestors.a)), b: get_z_index(find_stacking_context(ancestors.b)) }; if (z_indexes.a === z_indexes.b) { const children = common_ancestor.childNodes; const furthest_ancestors = { a: ancestors.a.at(-1), b: ancestors.b.at(-1) }; let i = children.length; while(i--){ const child = children[i]; if (child === furthest_ancestors.a) return 1; if (child === furthest_ancestors.b) return -1; } } return Math.sign(z_indexes.a - z_indexes.b); } const props = /\b(?:position|zIndex|opacity|transform|webkitTransform|mixBlendMode|filter|webkitFilter|isolation)\b/; /** @param {HTMLElement | SVGElement} node */ function is_flex_item(node) { var _get_parent; // @ts-ignore const display = getComputedStyle((_get_parent = get_parent(node)) !== null && _get_parent !== void 0 ? _get_parent : node).display; return display === "flex" || display === "inline-flex"; } /** @param {HTMLElement | SVGElement} node */ function creates_stacking_context(node) { const style = getComputedStyle(node); // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context if (style.position === "fixed") return true; // Forked to fix upstream bug https://github.com/Rich-Harris/stacking-order/issues/3 // if ( // (style.zIndex !== "auto" && style.position !== "static") || // is_flex_item(node) // ) if (style.zIndex !== "auto" && (style.position !== "static" || is_flex_item(node))) return true; if (+style.opacity < 1) return true; if ("transform" in style && style.transform !== "none") return true; if ("webkitTransform" in style && style.webkitTransform !== "none") return true; if ("mixBlendMode" in style && style.mixBlendMode !== "normal") return true; if ("filter" in style && style.filter !== "none") return true; if ("webkitFilter" in style && style.webkitFilter !== "none") return true; if ("isolation" in style && style.isolation === "isolate") return true; if (props.test(style.willChange)) return true; // @ts-expect-error if (style.webkitOverflowScrolling === "touch") return true; return false; } /** @param {(HTMLElement| SVGElement)[]} nodes */ function find_stacking_context(nodes) { let i = nodes.length; while(i--){ const node = nodes[i]; assert(node, "Missing node"); if (creates_stacking_context(node)) return node; } return null; } /** @param {HTMLElement | SVGElement} node */ function get_z_index(node) { return node && Number(getComputedStyle(node).zIndex) || 0; } /** @param {HTMLElement} node */ function get_ancestors(node) { const ancestors = []; while(node){ ancestors.push(node); // @ts-ignore node = get_parent(node); } return ancestors; // [ node, ... <body>, <html>, document ] } /** @param {HTMLElement} node */ function get_parent(node) { const { parentNode } = node; if (parentNode && parentNode instanceof ShadowRoot) { return parentNode.host; } return parentNode; } const EXCEEDED_HORIZONTAL_MIN = 0b0001; const EXCEEDED_HORIZONTAL_MAX = 0b0010; const EXCEEDED_VERTICAL_MIN = 0b0100; const EXCEEDED_VERTICAL_MAX = 0b1000; const isCoarsePointer = getInputType() === "coarse"; let intersectingHandles = []; let isPointerDown = false; let ownerDocumentCounts = new Map(); let panelConstraintFlags = new Map(); const registeredResizeHandlers = new Set(); function registerResizeHandle(resizeHandleId, element, direction, hitAreaMargins, setResizeHandlerState) { var _ownerDocumentCounts$; const { ownerDocument } = element; const data = { direction, element, hitAreaMargins, setResizeHandlerState }; const count = (_ownerDocumentCounts$ = ownerDocumentCounts.get(ownerDocument)) !== null && _ownerDocumentCounts$ !== void 0 ? _ownerDocumentCounts$ : 0; ownerDocumentCounts.set(ownerDocument, count + 1); registeredResizeHandlers.add(data); updateListeners(); return function unregisterResizeHandle() { var _ownerDocumentCounts$2; panelConstraintFlags.delete(resizeHandleId); registeredResizeHandlers.delete(data); const count = (_ownerDocumentCounts$2 = ownerDocumentCounts.get(ownerDocument)) !== null && _ownerDocumentCounts$2 !== void 0 ? _ownerDocumentCounts$2 : 1; ownerDocumentCounts.set(ownerDocument, count - 1); updateListeners(); if (count === 1) { ownerDocumentCounts.delete(ownerDocument); } // If the resize handle that is currently unmounting is intersecting with the pointer, // update the global pointer to account for the change if (intersectingHandles.includes(data)) { const index = intersectingHandles.indexOf(data); if (index >= 0) { intersectingHandles.splice(index, 1); } updateCursor(); // Also instruct the handle to stop dragging; this prevents the parent group from being left in an inconsistent state // See github.com/bvaughn/react-resizable-panels/issues/402 setResizeHandlerState("up", true, null); } }; } function handlePointerDown(event) { const { target } = event; const { x, y } = getResizeEventCoordinates(event); isPointerDown = true; recalculateIntersectingHandles({ target, x, y }); updateListeners(); if (intersectingHandles.length > 0) { updateResizeHandlerStates("down", event); // Update cursor based on return value(s) from active handles updateCursor(); event.preventDefault(); if (!isWithinResizeHandle(target)) { event.stopImmediatePropagation(); } } } function handlePointerMove(event) { const { x, y } = getResizeEventCoordinates(event); // Edge case (see #340) // Detect when the pointer has been released outside an iframe on a different domain if (isPointerDown && // Skip this check for "pointerleave" events, else Firefox triggers a false positive (see #514) event.type !== "pointerleave" && event.buttons === 0) { isPointerDown = false; updateResizeHandlerStates("up", event); } if (!isPointerDown) { const { target } = event; // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed // at that point, the handles may not move with the pointer (depending on constraints) // but the same set of active handles should be locked until the pointer is released recalculateIntersectingHandles({ target, x, y }); } updateResizeHandlerStates("move", event); // Update cursor based on return value(s) from active handles updateCursor(); if (intersectingHandles.length > 0) { event.preventDefault(); } } function handlePointerUp(event) { const { target } = event; const { x, y } = getResizeEventCoordinates(event); panelConstraintFlags.clear(); isPointerDown = false; if (intersectingHandles.length > 0) { event.preventDefault(); if (!isWithinResizeHandle(target)) { event.stopImmediatePropagation(); } } updateResizeHandlerStates("up", event); recalculateIntersectingHandles({ target, x, y }); updateCursor(); updateListeners(); } function isWithinResizeHandle(element) { let currentElement = element; while(currentElement){ if (currentElement.hasAttribute(DATA_ATTRIBUTES.resizeHandle)) { return true; } currentElement = currentElement.parentElement; } return false; } function recalculateIntersectingHandles({ target, x, y }) { intersectingHandles.splice(0); let targetElement = null; if (target instanceof HTMLElement || target instanceof SVGElement) { targetElement = target; } registeredResizeHandlers.forEach((data)=>{ const { element: dragHandleElement, hitAreaMargins } = data; const dragHandleRect = dragHandleElement.getBoundingClientRect(); const { bottom, left, right, top } = dragHandleRect; const margin = isCoarsePointer ? hitAreaMargins.coarse : hitAreaMargins.fine; const eventIntersects = x >= left - margin && x <= right + margin && y >= top - margin && y <= bottom + margin; if (eventIntersects) { // TRICKY // We listen for pointers events at the root in order to support hit area margins // (determining when the pointer is close enough to an element to be considered a "hit") // Clicking on an element "above" a handle (e.g. a modal) should prevent a hit though // so at this point we need to compare stacking order of a potentially intersecting drag handle, // and the element that was actually clicked/touched if (targetElement !== null && document.contains(targetElement) && dragHandleElement !== targetElement && !dragHandleElement.contains(targetElement) && !targetElement.contains(dragHandleElement) && // Calculating stacking order has a cost, so we should avoid it if possible // That is why we only check potentially intersecting handles, // and why we skip if the event target is within the handle's DOM compare(targetElement, dragHandleElement) > 0) { // If the target is above the drag handle, then we also need to confirm they overlap // If they are beside each other (e.g. a panel and its drag handle) then the handle is still interactive // // It's not enough to compare only the target // The target might be a small element inside of a larger container // (For example, a SPAN or a DIV inside of a larger modal dialog) let currentElement = targetElement; let didIntersect = false; while(currentElement){ if (currentElement.contains(dragHandleElement)) { break; } else if (intersects(currentElement.getBoundingClientRect(), dragHandleRect)) { didIntersect = true; break; } currentElement = currentElement.parentElement; } if (didIntersect) { return; } } intersectingHandles.push(data); } }); } function reportConstraintsViolation(resizeHandleId, flag) { panelConstraintFlags.set(resizeHandleId, flag); } function updateCursor() { let intersectsHorizontal = false; let intersectsVertical = false; intersectingHandles.forEach((data)=>{ const { direction } = data; if (direction === "horizontal") { intersectsHorizontal = true; } else { intersectsVertical = true; } }); let constraintFlags = 0; panelConstraintFlags.forEach((flag)=>{ constraintFlags |= flag; }); if (intersectsHorizontal && intersectsVertical) { setGlobalCursorStyle("intersection", constraintFlags); } else if (intersectsHorizontal) { setGlobalCursorStyle("horizontal", constraintFlags); } else if (intersectsVertical) { setGlobalCursorStyle("vertical", constraintFlags); } else { resetGlobalCursorStyle(); } } let listenersAbortController; function updateListeners() { var _listenersAbortContro; (_listenersAbortContro = listenersAbortController) === null || _listenersAbortContro === void 0 ? void 0 : _listenersAbortContro.abort(); listenersAbortController = new AbortController(); const options = { capture: true, signal: listenersAbortController.signal }; if (!registeredResizeHandlers.size) { return; } if (isPointerDown) { if (intersectingHandles.length > 0) { ownerDocumentCounts.forEach((count, ownerDocument)=>{ const { body } = ownerDocument; if (count > 0) { body.addEventListener("contextmenu", handlePointerUp, options); body.addEventListener("pointerleave", handlePointerMove, options); body.addEventListener("pointermove", handlePointerMove, options); } }); } ownerDocumentCounts.forEach((_, ownerDocument)=>{ const { body } = ownerDocument; body.addEventListener("pointerup", handlePointerUp, options); body.addEventListener("pointercancel", handlePointerUp, options); }); } else { ownerDocumentCounts.forEach((count, ownerDocument)=>{ const { body } = ownerDocument; if (count > 0) { body.addEventListener("pointerdown", handlePointerDown, options); body.addEventListener("pointermove", handlePointerMove, options); } }); } } function updateResizeHandlerStates(action, event) { registeredResizeHandlers.forEach((data)=>{ const { setResizeHandlerState } = data; const isActive = intersectingHandles.includes(data); setResizeHandlerState(action, isActive, event); }); } function useForceUpdate() { const [_, setCount] = useState(0); return useCallback(()=>setCount((prevCount)=>prevCount + 1), []); } function assert(expectedCondition, message) { if (!expectedCondition) { console.error(message); throw Error(message); } } function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) { if (actual.toFixed(fractionDigits) === expected.toFixed(fractionDigits)) { return 0; } else { return actual > expected ? 1 : -1; } } function fuzzyNumbersEqual$1(actual, expected, fractionDigits = PRECISION) { return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0; } function fuzzyNumbersEqual(actual, expected, fractionDigits) { return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0; } function fuzzyLayoutsEqual(actual, expected, fractionDigits) { if (actual.length !== expected.length) { return false; } for(let index = 0; index < actual.length; index++){ const actualSize = actual[index]; const expectedSize = expected[index]; if (!fuzzyNumbersEqual(actualSize, expectedSize, fractionDigits)) { return false; } } return true; } // Panel size must be in percentages; pixel values should be pre-converted function resizePanel({ panelConstraints: panelConstraintsArray, panelIndex, size }) { const panelConstraints = panelConstraintsArray[panelIndex]; assert(panelConstraints != null, `Panel constraints not found for index ${panelIndex}`); let { collapsedSize = 0, collapsible, maxSize = 100, minSize = 0 } = panelConstraints; if (fuzzyCompareNumbers(size, minSize) < 0) { if (collapsible) { // Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size. const halfwayPoint = (collapsedSize + minSize) / 2; if (fuzzyCompareNumbers(size, halfwayPoint) < 0) { size = collapsedSize; } else { size = minSize; } } else { size = minSize; } } size = Math.min(maxSize, size); size = parseFloat(size.toFixed(PRECISION)); return size; } // All units must be in percentages; pixel values should be pre-converted function adjustLayoutByDelta({ delta, initialLayout, panelConstraints: panelConstraintsArray, pivotIndices, prevLayout, trigger }) { if (fuzzyNumbersEqual(delta, 0)) { return initialLayout; } const nextLayout = [ ...initialLayout ]; const [firstPivotIndex, secondPivotIndex] = pivotIndices; assert(firstPivotIndex != null, "Invalid first pivot index"); assert(secondPivotIndex != null, "Invalid second pivot index"); let deltaApplied = 0; // const DEBUG = []; // DEBUG.push(`adjustLayoutByDelta()`); // DEBUG.push(` initialLayout: ${initialLayout.join(", ")}`); // DEBUG.push(` prevLayout: ${prevLayout.join(", ")}`); // DEBUG.push(` delta: ${delta}`); // DEBUG.push(` pivotIndices: ${pivotIndices.join(", ")}`); // DEBUG.push(` trigger: ${trigger}`); // DEBUG.push(""); // A resizing panel affects the panels before or after it. // // A negative delta means the panel(s) immediately after the resize handle should grow/expand by decreasing its offset. // Other panels may also need to shrink/contract (and shift) to make room, depending on the min weights. // // A positive delta means the panel(s) immediately before the resize handle should "expand". // This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the resize handle. { // If this is a resize triggered by a keyboard event, our logic for expanding/collapsing is different. // We no longer check the halfway threshold because this may prevent the panel from expanding at all. if (trigger === "keyboard") { { // Check if we should expand a collapsed panel const index = delta < 0 ? secondPivotIndex : firstPivotIndex; const panelConstraints = panelConstraintsArray[index]; assert(panelConstraints, `Panel constraints not found for index ${index}`); const { collapsedSize = 0, collapsible, minSize = 0 } = panelConstraints; // DEBUG.push(`edge case check 1: ${index}`); // DEBUG.push(` -> collapsible? ${collapsible}`); if (collapsible) { const prevSize = initialLayout[index]; assert(prevSize != null, `Previous layout not found for panel index ${index}`); if (fuzzyNumbersEqual(prevSize, collapsedSize)) { const localDelta = minSize - prevSize; // DEBUG.push(` -> expand delta: ${localDelta}`); if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { delta = delta < 0 ? 0 - localDelta : localDelta; // DEBUG.push(` -> delta: ${delta}`); } } } } { // Check if we should collapse a panel at its minimum size const index = delta < 0 ? firstPivotIndex : secondPivotIndex; const panelConstraints = panelConstraintsArray[index]; assert(panelConstraints, `No panel constraints found for index ${index}`); const { collapsedSize = 0, collapsible, minSize = 0 } = panelConstraints; // DEBUG.push(`edge case check 2: ${index}`); // DEBUG.push(` -> collapsible? ${collapsible}`); if (collapsible) { const prevSize = initialLayout[index]; assert(prevSize != null, `Previous layout not found for panel index ${index}`); if (fuzzyNumbersEqual(prevSize, minSize)) { const localDelta = prevSize - collapsedSize; // DEBUG.push(` -> expand delta: ${localDelta}`); if (fuzzyCompareNumbers(localDelta, Math.abs(delta)) > 0) { delta = delta < 0 ? 0 - localDelta : localDelta; // DEBUG.push(` -> delta: ${delta}`); } } } } } // DEBUG.push(""); } { // Pre-calculate max available delta in the opposite direction of our pivot. // This will be the maximum amount we're allowed to expand/contract the panels in the primary direction. // If this amount is less than the requested delta, adjust the requested delta. // If this amount is greater than the requested delta, that's useful information too– // as an expanding panel might change from collapsed to min size. const increment = delta < 0 ? 1 : -1; let index = delta < 0 ? secondPivotIndex : firstPivotIndex; let maxAvailableDelta = 0; // DEBUG.push("pre calc..."); while(true){ const prevSize = initialLayout[index]; assert(prevSize != null, `Previous layout not found for panel index ${index}`); const maxSafeSize = resizePanel({ panelConstraints: panelConstraintsArray, panelIndex: index, size: 100 }); const delta = maxSafeSize - prevSize; // DEBUG.push(` ${index}: ${prevSize} -> ${maxSafeSize}`); maxAvailableDelta += delta; index += increment; if (index < 0 || index >= panelConstraintsArray.length) { break; } } // DEBUG.push(` -> max available delta: ${maxAvailableDelta}`); const minAbsDelta = Math.min(Math.abs(delta), Math.abs(maxAvailableDelta)); delta = delta < 0 ? 0 - minAbsDelta : minAbsDelta; // DEBUG.push(` -> adjusted delta: ${delta}`); // DEBUG.push(""); } { // Delta added to a panel needs to be subtracted from other panels (within the constraints that those panels allow). const pivotIndex = delta < 0 ? firstPivotIndex : secondPivotIndex; let index = pivotIndex; while(index >= 0 && index < panelConstraintsArray.length){ const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied); const prevSize = initialLayout[index]; assert(prevSize != null, `Previous layout not found for panel index ${index}`); const unsafeSize = prevSize - deltaRemaining; const safeSize = resizePanel({ panelConstraints: panelConstraintsArray, panelIndex: index, size: unsafeSize }); if (!fuzzyNumbersEqual(prevSize, safeSize)) { deltaApplied += prevSize - safeSize; nextLayout[index] = safeSize; if (deltaApplied.toFixed(3).localeCompare(Math.abs(delta).toFixed(3), undefined, { numeric: true }) >= 0) { break; } } if (delta < 0) { index--; } else { index++; } } } // DEBUG.push(`after 1: ${nextLayout.join(", ")}`); // DEBUG.push(` deltaApplied: ${deltaApplied}`); // DEBUG.push(""); // If we were unable to resize any of the panels panels, return the previous state. // This will essentially bailout and ignore e.g. drags past a panel's boundaries if (fuzzyLayoutsEqual(prevLayout, nextLayout)) { // DEBUG.push(`bailout to previous layout: ${prevLayout.join(", ")}`); // console.log(DEBUG.join("\n")); return prevLayout; } { // Now distribute the applied delta to the panels in the other direction const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex; const prevSize = initialLayout[pivotIndex]; assert(prevSize != null, `Previous layout not found for panel index ${pivotIndex}`); const unsafeSize = prevSize + deltaApplied; const safeSize = resizePanel({ panelConstraints: panelConstraintsArray, panelIndex: pivotIndex, size: unsafeSize }); // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract. nextLayout[pivotIndex] = safeSize; // Edge case where expanding or contracting one panel caused another one to change collapsed state if (!fuzzyNumbersEqual(safeSize, unsafeSize)) { let deltaRemaining = unsafeSize - safeSize; const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex; let index = pivotIndex; while(index >= 0 && index < panelConstraintsArray.length){ const prevSize = nextLayout[index]; assert(prevSize != null, `Previous layout not found for panel index ${index}`); const unsafeSize = prevSize + deltaRemaining; const safeSize = resizePanel({ panelConstraints: panelConstraintsArray, panelIndex: index, size: unsafeSize }); if (!fuzzyNumbersEqual(prevSize, safeSize)) { deltaRemaining -= safeSize - prevSize; nextLayout[index] = safeSize; } if (fuzzyNumbersEqual(deltaRemaining, 0)) { break; } if (delta > 0) { index--; } else { index++; } } } } // DEBUG.push(`after 2: ${nextLayout.join(", ")}`); // DEBUG.push(` deltaApplied: ${deltaApplied}`); // DEBUG.push(""); const totalSize = nextLayout.reduce((total, size)=>size + total, 0); // DEBUG.push(`total size: ${totalSize}`); // If our new layout doesn't add up to 100%, that means the requested delta can't be applied // In that case, fall back to our most recent valid layout if (!fuzzyNumbersEqual(totalSize, 100)) { // DEBUG.push(`bailout to previous layout: ${prevLayout.join(", ")}`); // console.log(DEBUG.join("\n")); return prevLayout; } // console.log(DEBUG.join("\n")); return nextLayout; } function calculateAriaValues({ layout, panelsArray, pivotIndices }) { let currentMinSize = 0; let currentMaxSize = 100; let totalMinSize = 0; let totalMaxSize = 0; const firstIndex = pivotIndices[0]; assert(firstIndex != null, "No pivot index found"); // A panel's effective min/max sizes also need to account for other panel's sizes. panelsArray.forEach((panelData, index)=>{ const { constraints } = panelData; const { maxSize = 100, minSize = 0 } = constraints; if (index === firstIndex) { currentMinSize = minSize; currentMaxSize = maxSize; } else { totalMinSize += minSize; totalMaxSize += maxSize; } }); const valueMax = Math.min(currentMaxSize, 100 - totalMinSize); const valueMin = Math.max(currentMinSize, 100 - totalMaxSize); const valueNow = layout[firstIndex]; return { valueMax, valueMin, valueNow }; } function getResizeHandleElementsForGroup(groupId, scope = document) { return Array.from(scope.querySelectorAll(`[${DATA_ATTRIBUTES.resizeHandleId}][data-panel-group-id="${groupId}"]`)); } function getResizeHandleElementIndex(groupId, id, scope = document) { const handles = getResizeHandleElementsForGroup(groupId, scope); const index = handles.findIndex((handle)=>handle.getAttribute(DATA_ATTRIBUTES.resizeHandleId) === id); return index !== null && index !== void 0 ? index : null; } function determinePivotIndices(groupId, dragHandleId, panelGroupElement) { const index = getResizeHandleElementIndex(groupId, dragHandleId, panelGroupElement); return index != null ? [ index, index + 1 ] : [ -1, -1 ]; } function isHTMLElement(target) { if (target instanceof HTMLElement) { return true; } // Fallback to duck typing to handle edge case of portals within a popup window return typeof target === "object" && target !== null && "tagName" in target && "getAttribute" in target; } function getPanelGroupElement(id, rootElement = document) { // If the root element is the PanelGroup if (isHTMLElement(rootElement) && rootElement.dataset.panelGroupId == id) { return rootElement; } // Else query children const element = rootElement.querySelector(`[data-panel-group][data-panel-group-id="${id}"]`); if (element) { return element; } return null; } function getResizeHandleElement(id, scope = document) { const element = scope.querySelector(`[${DATA_ATTRIBUTES.resizeHandleId}="${id}"]`); if (element) { return element; } return null; } function getResizeHandlePanelIds(groupId, handleId, panelsArray, scope = document) { var _panelsArray$index$id, _panelsArray$index, _panelsArray$id, _panelsArray; const handle = getResizeHandleElement(handleId, scope); const handles = getResizeHandleElementsForGroup(groupId, scope); const index = handle ? handles.indexOf(handle) : -1; const idBefore = (_panelsArray$index$id = (_panelsArray$index = panelsArray[index]) === null || _panelsArray$index === void 0 ? void 0 : _panelsArray$index.id) !== null && _panelsArray$index$id !== void 0 ? _panelsArray$index$id : null; const idAfter = (_panelsArray$id = (_panelsArray = panelsArray[index + 1]) === null || _panelsArray === void 0 ? void 0 : _panelsArray.id) !== null && _panelsArray$id !== void 0 ? _panelsArray$id : null; return [ idBefore, idAfter ]; } // https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ function useWindowSplitterPanelGroupBehavior({ committedValuesRef, eagerValuesRef, groupId, layout, panelDataArray, panelGroupElement, setLayout }) { useRef({ didWarnAboutMissingResizeHandle: false }); useIsomorphicLayoutEffect(()=>{ if (!panelGroupElement) { return; } const resizeHandleElements = getResizeHandleElementsForGroup(groupId, panelGroupElement); for(let index = 0; index < panelDataArray.length - 1; index++){ const { valueMax, valueMin, valueNow } = calculateAriaValues({ layout, panelsArray: panelDataArray, pivotIndices: [ index, index + 1 ] }); const resizeHandleElement = resizeHandleElements[index]; if (resizeHandleElement == null) ; else { const panelData = panelDataArray[index]; assert(panelData, `No panel data found for index "${index}"`); resizeHandleElement.setAttribute("aria-controls", panelData.id); resizeHandleElement.setAttribute("aria-valuemax", "" + Math.round(valueMax)); resizeHandleElement.setAttribute("aria-valuemin", "" + Math.round(valueMin)); resizeHandleElement.setAttribute("aria-valuenow", valueNow != null ? "" + Math.round(valueNow) : ""); } } return ()=>{ resizeHandleElements.forEach((resizeHandleElement, index)=>{ resizeHandleElement.removeAttribute("aria-controls"); resizeHandleElement.removeAttribute("aria-valuemax"); resizeHandleElement.removeAttribute("aria-valuemin"); resizeHandleElement.removeAttribute("aria-valuenow"); }); }; }, [ groupId, layout, panelDataArray, panelGroupElement ]); useEffect(()=>{ if (!panelGroupElement) { return; } const eagerValues = eagerValuesRef.current; assert(eagerValues, `Eager values not found`); const { panelDataArray } = eagerValues; const groupElement = getPanelGroupElement(groupId, panelGroupElement); assert(groupElement != null, `No group found for id "${groupId}"`); const handles = getResizeHandleElementsForGroup(groupId, panelGroupElement); assert(handles, `No resize handles found for group id "${groupId}"`); const cleanupFunctions = handles.map((handle)=>{ const handleId = handle.getAttribute(DATA_ATTRIBUTES.resizeHandleId); assert(handleId, `Resize handle element has no handle id attribute`); const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelDataArray, panelGroupElement); if (idBefore == null || idAfter == null) { return ()=>{}; } const onKeyDown = (event)=>{ if (event.defaultPrevented) { return; } switch(event.key){ case "Enter": { event.preventDefault(); const index = panelDataArray.findIndex((panelData)=>panelData.id === idBefore); if (index >= 0) { const panelData = panelDataArray[index]; assert(panelData, `No panel data found for index ${index}`); const size = layout[index]; const { collapsedSize = 0, collapsible, minSize = 0 } = panelData.constraints; if (size != null && collapsible) { const nextLayout = adjustLayoutByDelta({ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size, initialLayout: layout, panelConstraints: panelDataArray.map((panelData)=>panelData.constraints), pivotIndices: determinePivotIndices(groupId, handleId, panelGroupElement), prevLayout: layout, trigger: "keyboard" }); if (layout !== nextLayout) { setLayout(nextLayout); } } } break; } } }; handle.addEventListener("keydown", onKeyDown); return ()=>{ handle.removeEventListener("keydown", onKeyDown); }; }); return ()=>{ cleanupFunctions.forEach((cleanupFunction)=>cleanupFunction()); }; }, [ panelGroupElement, committedValuesRef, eagerValuesRef, groupId, layout, panelDataArray, setLayout ]); } function areEqual(arrayA, arrayB) { if (arrayA.length !== arrayB.length) { return false; } for(let index = 0; index < arrayA.length; index++){ if (arrayA[index] !== arrayB[index]) { return false; } } return true; } function getResizeEventCursorPosition(direction, event) { const isHorizontal = direction === "horizontal"; const { x, y } = getResizeEventCoordinates(event); return isHorizontal ? x : y; } function calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState, panelGroupElement) { const isHorizontal = direction === "horizontal"; const handleElement = getResizeHandleElement(dragHandleId, panelGroupElement); assert(handleElement, `No resize handle element found for id "${dragHandleId}"`); const groupId = handleElement.getAttribute(DATA_ATTRIBUTES.groupId); assert(groupId, `Resize handle element has no group id attribute`); let { initialCursorPosition } = initialDragState; const cursorPosition = getResizeEventCursorPosition(direction, event); const groupElement = getPanelGroupElement(groupId, panelGroupElement); assert(groupElement, `No group element found for id "${groupId}"`); const groupRect = groupElement.getBoundingClientRect(); const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height; const offsetPixels = cursorPosition - initialCursorPosition; const offsetPercentage = offsetPixels / groupSizeInPixels * 100; return offsetPercentage; } // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX function calculateDeltaPercentage(event, dragHandleId, direction, initialDragState, keyboardResizeBy, panelGroupElement) { if (isKeyDown(event)) { const isHorizontal = direction === "horizontal"; let delta = 0; if (event.shiftKey) { delta = 100; } else if (keyboardResizeBy != null) { delta = keyboardResizeBy; } else { delta = 10; } let movement = 0; switch(event.key){ case "ArrowDown": movement = isHorizontal ? 0 : delta; break; case "ArrowLeft": movement = isHorizontal ? -delta : 0; break; case "ArrowRight": movement = isHorizontal ? delta : 0; break; case "ArrowUp": movement = isHorizontal ? 0 : -delta; break; case "End": movement = 100; break; case "Home": movement = -100; break; } return movement; } else { if (initialDragState == null) { return 0; } return calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState, panelGroupElement); } } function calculateUnsafeDefaultLayout({ panelDataArray }) { const layout = Array(panelDataArray.length); const panelConstraintsArray = panelDataArray.map((panelData)=>panelData.constraints); let numPanelsWithSizes = 0; let remainingSize = 100; // Distribute default sizes first for(let index = 0; index < panelDataArray.length; index++){ const panelConstraints = panelConstraintsArray[index]; assert(panelConstraints, `Panel constraints not found for index ${index}`); const { defaultSize } = panelConstraints; if (defaultSize != null) { numPanelsWithSizes++; layout[index] = defaultSize; remainingSize -= defaultSize; } } // Remaining size should be distributed evenly between panels without default sizes for(let index = 0; index < panelDataArray.length; index++){ const panelConstraints = panelConstraintsArray[index]; assert(panelConstraints, `Panel constraints not found for index ${index