ivt
Version:
Ivt Components Library
1,206 lines (1,204 loc) • 92.8 kB
JavaScript
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