react-resizable-panels
Version:
<img src="https://react-resizable-panels.vercel.app/og.png" alt="react-resizable-panels logo" width="400" height="210" />
1 lines • 198 kB
Source Map (JSON)
{"version":3,"file":"react-resizable-panels.cjs","sources":["../lib/global/styles/convertEmToPixels.ts","../lib/global/styles/convertRemToPixels.ts","../lib/global/styles/convertVhToPixels.ts","../lib/global/styles/convertVwToPixels.ts","../lib/global/styles/parseSizeAndUnit.ts","../lib/global/styles/sizeStyleToPixels.ts","../lib/global/utils/formatLayoutNumber.ts","../lib/global/dom/calculateAvailableGroupSize.ts","../lib/global/dom/calculatePanelConstraints.ts","../lib/utils/assert.ts","../lib/components/group/sortByElementOffset.ts","../lib/utils/isHTMLElement.ts","../lib/global/utils/getDistanceBetweenPointAndRect.ts","../lib/global/utils/findClosestRect.ts","../lib/global/utils/isCoarsePointer.ts","../lib/global/dom/calculateHitRegions.ts","../lib/utils/EventEmitter.ts","../lib/global/mutable-state/groups.ts","../lib/global/utils/findClosestHitRegion.ts","../lib/utils/isShadowRoot.ts","../lib/vendor/stacking-order.ts","../lib/global/utils/doRectsIntersect.ts","../lib/global/utils/isViableHitTarget.ts","../lib/global/utils/findMatchingHitRegions.ts","../lib/utils/isArrayEqual.ts","../lib/global/utils/layoutNumbersEqual.ts","../lib/global/utils/compareLayoutNumbers.ts","../lib/global/utils/validatePanelSize.ts","../lib/global/utils/adjustLayoutByDelta.ts","../lib/global/utils/layoutsEqual.ts","../lib/global/utils/validatePanelGroupLayout.ts","../lib/global/utils/getImperativePanelMethods.ts","../lib/global/event-handlers/onDocumentDoubleClick.ts","../lib/global/utils/findSeparatorGroup.ts","../lib/global/utils/getImperativeGroupMethods.ts","../lib/global/utils/adjustLayoutForSeparator.ts","../lib/global/event-handlers/onDocumentKeyDown.ts","../lib/global/mutable-state/interactions.ts","../lib/global/event-handlers/onDocumentPointerDown.ts","../lib/constants.ts","../lib/global/cursor/supportsAdvancedCursorStyles.ts","../lib/global/cursor/getCursorStyle.ts","../lib/global/cursor/updateCursorStyle.ts","../lib/global/utils/updateActiveHitRegion.ts","../lib/global/event-handlers/onDocumentPointerLeave.ts","../lib/global/event-handlers/onDocumentPointerMove.ts","../lib/global/event-handlers/onDocumentPointerOut.ts","../lib/global/event-handlers/onDocumentPointerUp.ts","../lib/global/utils/calculateDefaultLayout.ts","../lib/global/utils/notifyPanelOnResize.ts","../lib/global/utils/objectsEqual.ts","../lib/global/utils/preserveFixedPanelSizes.ts","../lib/global/utils/validateLayoutKeys.ts","../lib/global/mountGroup.ts","../lib/hooks/useForceUpdate.ts","../lib/hooks/useId.ts","../lib/hooks/useIsomorphicLayoutEffect.ts","../lib/hooks/useStableCallback.ts","../lib/hooks/useMergedRefs.ts","../lib/hooks/useStableObject.ts","../lib/components/group/GroupContext.ts","../lib/components/group/useGroupImperativeHandle.ts","../lib/components/group/Group.tsx","../lib/components/group/auto-save/getStorageKey.ts","../lib/components/group/readLegacyLayout.ts","../lib/components/group/useDefaultLayout.ts","../lib/components/group/useGroupCallbackRef.ts","../lib/components/group/useGroupRef.ts","../lib/components/group/useGroupContext.ts","../lib/components/panel/usePanelImperativeHandle.ts","../lib/components/panel/Panel.tsx","../lib/components/panel/usePanelCallbackRef.ts","../lib/components/panel/usePanelRef.ts","../lib/global/utils/calculateSeparatorAriaValues.ts","../lib/components/separator/Separator.tsx"],"sourcesContent":["export function convertEmToPixels(element: Element, value: number) {\n const style = getComputedStyle(element);\n const fontSize = parseFloat(style.fontSize);\n\n return value * fontSize;\n}\n","export function convertRemToPixels(element: Element, value: number) {\n const style = getComputedStyle(element.ownerDocument.body);\n const fontSize = parseFloat(style.fontSize);\n\n return value * fontSize;\n}\n","export function convertVhToPixels(value: number) {\n return (value / 100) * window.innerHeight;\n}\n","export function convertVwToPixels(value: number) {\n return (value / 100) * window.innerWidth;\n}\n","import type { SizeUnit } from \"../../components/panel/types\";\n\nexport function parseSizeAndUnit(\n size: number | string\n): [numeric: number, size: SizeUnit] {\n switch (typeof size) {\n case \"number\": {\n return [size, \"px\"];\n }\n case \"string\": {\n const numeric = parseFloat(size);\n\n if (size.endsWith(\"%\")) {\n return [numeric, \"%\"];\n } else if (size.endsWith(\"px\")) {\n return [numeric, \"px\"];\n } else if (size.endsWith(\"rem\")) {\n return [numeric, \"rem\"];\n } else if (size.endsWith(\"em\")) {\n return [numeric, \"em\"];\n } else if (size.endsWith(\"vh\")) {\n return [numeric, \"vh\"];\n } else if (size.endsWith(\"vw\")) {\n return [numeric, \"vw\"];\n }\n\n return [numeric, \"%\"];\n }\n }\n}\n","import { convertEmToPixels } from \"./convertEmToPixels\";\nimport { convertRemToPixels } from \"./convertRemToPixels\";\nimport { convertVhToPixels } from \"./convertVhToPixels\";\nimport { convertVwToPixels } from \"./convertVwToPixels\";\nimport { parseSizeAndUnit } from \"./parseSizeAndUnit\";\n\nexport function sizeStyleToPixels({\n groupSize,\n panelElement,\n styleProp\n}: {\n groupSize: number;\n panelElement: HTMLElement;\n styleProp: number | string;\n}) {\n let pixels: number | undefined = undefined;\n\n const [size, unit] = parseSizeAndUnit(styleProp);\n\n switch (unit) {\n case \"%\": {\n pixels = (size / 100) * groupSize;\n break;\n }\n case \"px\": {\n pixels = size;\n break;\n }\n case \"rem\": {\n pixels = convertRemToPixels(panelElement, size);\n break;\n }\n case \"em\": {\n pixels = convertEmToPixels(panelElement, size);\n break;\n }\n case \"vh\": {\n pixels = convertVhToPixels(size);\n break;\n }\n case \"vw\": {\n pixels = convertVwToPixels(size);\n break;\n }\n }\n\n return pixels;\n}\n","export function formatLayoutNumber(number: number) {\n return parseFloat(number.toFixed(3));\n}\n","import type { RegisteredGroup } from \"../../components/group/types\";\n\nexport function calculateAvailableGroupSize({\n group\n}: {\n group: RegisteredGroup;\n}) {\n const { orientation, panels } = group;\n\n return panels.reduce((totalSize, panel) => {\n totalSize +=\n orientation === \"horizontal\"\n ? panel.element.offsetWidth\n : panel.element.offsetHeight;\n return totalSize;\n }, 0);\n}\n","import type { RegisteredGroup } from \"../../components/group/types\";\nimport type { PanelConstraints } from \"../../components/panel/types\";\nimport { sizeStyleToPixels } from \"../styles/sizeStyleToPixels\";\nimport { formatLayoutNumber } from \"../utils/formatLayoutNumber\";\nimport { calculateAvailableGroupSize } from \"./calculateAvailableGroupSize\";\n\nexport function calculatePanelConstraints(group: RegisteredGroup) {\n const { panels } = group;\n\n const groupSize = calculateAvailableGroupSize({ group });\n if (groupSize === 0) {\n // Can't calculate anything meaningful if the group has a width/height of 0\n // (This could indicate that it's within a hidden subtree)\n return panels.map<PanelConstraints>((current) => ({\n groupResizeBehavior: current.panelConstraints.groupResizeBehavior,\n collapsedSize: 0,\n collapsible: current.panelConstraints.collapsible === true,\n defaultSize: undefined,\n disabled: current.panelConstraints.disabled,\n minSize: 0,\n maxSize: 100,\n panelId: current.id\n }));\n }\n\n return panels.map<PanelConstraints>((panel) => {\n const { element, panelConstraints } = panel;\n\n let collapsedSize = 0;\n if (panelConstraints.collapsedSize !== undefined) {\n const pixels = sizeStyleToPixels({\n groupSize,\n panelElement: element,\n styleProp: panelConstraints.collapsedSize\n });\n\n collapsedSize = formatLayoutNumber((pixels / groupSize) * 100);\n }\n\n let defaultSize: number | undefined = undefined;\n if (panelConstraints.defaultSize !== undefined) {\n const pixels = sizeStyleToPixels({\n groupSize,\n panelElement: element,\n styleProp: panelConstraints.defaultSize\n });\n\n defaultSize = formatLayoutNumber((pixels / groupSize) * 100);\n }\n\n let minSize = 0;\n if (panelConstraints.minSize !== undefined) {\n const pixels = sizeStyleToPixels({\n groupSize,\n panelElement: element,\n styleProp: panelConstraints.minSize\n });\n\n minSize = formatLayoutNumber((pixels / groupSize) * 100);\n }\n\n let maxSize = 100;\n if (panelConstraints.maxSize !== undefined) {\n const pixels = sizeStyleToPixels({\n groupSize,\n panelElement: element,\n styleProp: panelConstraints.maxSize\n });\n\n maxSize = formatLayoutNumber((pixels / groupSize) * 100);\n }\n\n return {\n groupResizeBehavior: panelConstraints.groupResizeBehavior,\n collapsedSize,\n collapsible: panelConstraints.collapsible === true,\n defaultSize,\n disabled: panelConstraints.disabled,\n minSize,\n maxSize,\n panelId: panel.id\n };\n });\n}\n","export function assert(\n expectedCondition: unknown,\n message: string = \"Assertion error\"\n): asserts expectedCondition {\n if (!expectedCondition) {\n throw Error(message);\n }\n}\n","import type { Orientation } from \"./types\";\n\nexport function sortByElementOffset<\n Type extends { element: HTMLElement },\n ReturnType extends Type[]\n>(orientation: Orientation, panelsOrSeparators: Type[]): ReturnType {\n return Array.from(panelsOrSeparators).sort(\n orientation === \"horizontal\" ? horizontalSort : verticalSort\n ) as ReturnType;\n}\n\nfunction horizontalSort<Type extends { element: HTMLElement }>(\n a: Type,\n b: Type\n) {\n const delta = a.element.offsetLeft - b.element.offsetLeft;\n if (delta !== 0) {\n return delta;\n }\n return a.element.offsetWidth - b.element.offsetWidth;\n}\n\nfunction verticalSort<Type extends { element: HTMLElement }>(a: Type, b: Type) {\n const delta = a.element.offsetTop - b.element.offsetTop;\n if (delta !== 0) {\n return delta;\n }\n return a.element.offsetHeight - b.element.offsetHeight;\n}\n","// Detects HTMLElement without requiring instanceof and browser globals\nexport function isHTMLElement(value: unknown): value is HTMLElement {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"nodeType\" in value &&\n value.nodeType === Node.ELEMENT_NODE\n );\n}\n","import type { Point } from \"../../types\";\n\nexport function getDistanceBetweenPointAndRect(\n point: Point,\n rect: DOMRectReadOnly\n) {\n return {\n x:\n point.x >= rect.left && point.x <= rect.right\n ? 0\n : Math.min(\n Math.abs(point.x - rect.left),\n Math.abs(point.x - rect.right)\n ),\n y:\n point.y >= rect.top && point.y <= rect.bottom\n ? 0\n : Math.min(\n Math.abs(point.y - rect.top),\n Math.abs(point.y - rect.bottom)\n )\n };\n}\n","import type { Orientation } from \"../../components/group/types\";\nimport { assert } from \"../../utils/assert\";\nimport { getDistanceBetweenPointAndRect } from \"./getDistanceBetweenPointAndRect\";\n\nexport function findClosestRect({\n orientation,\n rects,\n targetRect\n}: {\n orientation: Orientation;\n rects: DOMRectReadOnly[];\n targetRect: DOMRectReadOnly;\n}): DOMRectReadOnly {\n const centerPoint = {\n x: targetRect.x + targetRect.width / 2,\n y: targetRect.y + targetRect.height / 2\n };\n\n let closestRect: DOMRectReadOnly | undefined = undefined;\n let minDistance = Number.MAX_VALUE;\n\n for (const rect of rects) {\n const { x, y } = getDistanceBetweenPointAndRect(centerPoint, rect);\n\n const distance = orientation === \"horizontal\" ? x : y;\n\n if (distance < minDistance) {\n minDistance = distance;\n closestRect = rect;\n }\n }\n\n assert(closestRect, \"No rect found\");\n\n return closestRect;\n}\n","let cached: boolean | undefined = undefined;\n\n/**\n * Caches and returns matchMedia()'s computed value for \"pointer:coarse\"\n */\nexport function isCoarsePointer(): boolean {\n if (cached === undefined) {\n if (typeof matchMedia === \"function\") {\n cached = !!matchMedia(\"(pointer:coarse)\").matches;\n } else {\n cached = false;\n }\n }\n\n return cached;\n}\n","import { sortByElementOffset } from \"../../components/group/sortByElementOffset\";\nimport type { RegisteredGroup } from \"../../components/group/types\";\nimport type { RegisteredPanel } from \"../../components/panel/types\";\nimport type { RegisteredSeparator } from \"../../components/separator/types\";\nimport { isHTMLElement } from \"../../utils/isHTMLElement\";\nimport { findClosestRect } from \"../utils/findClosestRect\";\nimport { isCoarsePointer } from \"../utils/isCoarsePointer\";\nimport { calculateAvailableGroupSize } from \"./calculateAvailableGroupSize\";\n\ntype PanelsTuple = [panel: RegisteredPanel, panel: RegisteredPanel];\n\nexport type HitRegion = {\n group: RegisteredGroup;\n groupSize: number;\n panels: PanelsTuple;\n rect: DOMRect;\n separator?: RegisteredSeparator | undefined;\n};\n\n/**\n * Determines hit regions for a Group; a hit region is either:\n * - 1: An explicit Separator element\n * - 2: The edge of a Panel element that has another Panel beside it\n *\n * This method determines bounding rects of all regions for the particular group.\n */\nexport function calculateHitRegions(group: RegisteredGroup) {\n const { element: groupElement, orientation, panels, separators } = group;\n\n // Sort elements by offset before traversing\n const sortedChildElements: HTMLElement[] = sortByElementOffset(\n orientation,\n Array.from(groupElement.children)\n .filter(isHTMLElement)\n .map((element) => ({ element: element as HTMLElement }))\n ).map(({ element }) => element);\n\n const hitRegions: HitRegion[] = [];\n\n let disabledSeparator = false;\n let hasInterleavedStaticContent = false;\n let firstEnabledPanelIndex = -1;\n let lastEnabledPanelIndex = -1;\n let numEnabledPanels = 0;\n let prevPanel: RegisteredPanel | undefined = undefined;\n let pendingSeparators: RegisteredSeparator[] = [];\n\n {\n let currentPanelIndex = -1;\n\n for (const childElement of sortedChildElements) {\n if (childElement.hasAttribute(\"data-panel\")) {\n currentPanelIndex++;\n\n if (!childElement.hasAttribute(\"data-disabled\")) {\n numEnabledPanels++;\n\n if (firstEnabledPanelIndex === -1) {\n firstEnabledPanelIndex = currentPanelIndex;\n }\n\n lastEnabledPanelIndex = currentPanelIndex;\n }\n }\n }\n }\n\n // If all (or all but one) of the Panels are disabled, there can be no resize interactions.\n if (numEnabledPanels > 1) {\n let currentPanelIndex = -1;\n\n for (const childElement of sortedChildElements) {\n if (childElement.hasAttribute(\"data-panel\")) {\n currentPanelIndex++;\n\n const panelData = panels.find(\n (current) => current.element === childElement\n );\n if (panelData) {\n if (prevPanel) {\n const prevRect = prevPanel.element.getBoundingClientRect();\n const rect = childElement.getBoundingClientRect();\n\n let pendingRectsOrSeparators: (DOMRect | RegisteredSeparator)[];\n\n // If an explicit Separator has been rendered, always watch it\n // Otherwise watch the entire space between the panels\n // The one caveat is when there are non-interactive element(s) between panels,\n // in which case we may need to watch individual panel edges\n if (hasInterleavedStaticContent) {\n const firstPanelEdgeRect =\n orientation === \"horizontal\"\n ? new DOMRect(\n prevRect.right,\n prevRect.top,\n 0,\n prevRect.height\n )\n : new DOMRect(\n prevRect.left,\n prevRect.bottom,\n prevRect.width,\n 0\n );\n const secondPanelEdgeRect =\n orientation === \"horizontal\"\n ? new DOMRect(rect.left, rect.top, 0, rect.height)\n : new DOMRect(rect.left, rect.top, rect.width, 0);\n\n switch (pendingSeparators.length) {\n case 0: {\n pendingRectsOrSeparators = [\n firstPanelEdgeRect,\n secondPanelEdgeRect\n ];\n break;\n }\n case 1: {\n const separator = pendingSeparators[0];\n const closestRect = findClosestRect({\n orientation,\n rects: [prevRect, rect],\n targetRect: separator.element.getBoundingClientRect()\n });\n\n pendingRectsOrSeparators = [\n separator,\n closestRect === prevRect\n ? secondPanelEdgeRect\n : firstPanelEdgeRect\n ];\n break;\n }\n default: {\n pendingRectsOrSeparators = pendingSeparators;\n break;\n }\n }\n } else {\n if (pendingSeparators.length) {\n pendingRectsOrSeparators = pendingSeparators;\n } else {\n pendingRectsOrSeparators = [\n orientation === \"horizontal\"\n ? new DOMRect(\n prevRect.right,\n rect.top,\n rect.left - prevRect.right,\n rect.height\n )\n : new DOMRect(\n rect.left,\n prevRect.bottom,\n rect.width,\n rect.top - prevRect.bottom\n )\n ];\n }\n }\n\n for (const rectOrSeparator of pendingRectsOrSeparators) {\n let rect =\n \"width\" in rectOrSeparator\n ? rectOrSeparator\n : rectOrSeparator.element.getBoundingClientRect();\n\n const minHitTargetSize = isCoarsePointer()\n ? group.resizeTargetMinimumSize.coarse\n : group.resizeTargetMinimumSize.fine;\n if (rect.width < minHitTargetSize) {\n const delta = minHitTargetSize - rect.width;\n rect = new DOMRect(\n rect.x - delta / 2,\n rect.y,\n rect.width + delta,\n rect.height\n );\n }\n if (rect.height < minHitTargetSize) {\n const delta = minHitTargetSize - rect.height;\n rect = new DOMRect(\n rect.x,\n rect.y - delta / 2,\n rect.width,\n rect.height + delta\n );\n }\n\n const skip =\n currentPanelIndex <= firstEnabledPanelIndex ||\n currentPanelIndex > lastEnabledPanelIndex;\n\n if (!disabledSeparator && !skip) {\n hitRegions.push({\n group,\n groupSize: calculateAvailableGroupSize({ group }),\n panels: [prevPanel, panelData],\n separator:\n \"width\" in rectOrSeparator ? undefined : rectOrSeparator,\n rect\n });\n }\n\n disabledSeparator = false;\n }\n }\n\n hasInterleavedStaticContent = false;\n prevPanel = panelData;\n pendingSeparators = [];\n }\n } else if (childElement.hasAttribute(\"data-separator\")) {\n if (childElement.ariaDisabled !== null) {\n disabledSeparator = true;\n }\n\n const separatorData = separators.find(\n (current) => current.element === childElement\n );\n if (separatorData) {\n // Separators will be included implicitly in the area between the previous and next panel\n // It's important to track them though, to handle the scenario of non-interactive group content\n pendingSeparators.push(separatorData);\n } else {\n prevPanel = undefined;\n pendingSeparators = [];\n }\n } else {\n hasInterleavedStaticContent = true;\n }\n }\n }\n\n return hitRegions;\n}\n","export type EventMap = {\n [key: string]: unknown;\n};\n\nexport type EventListener<Data> = (data: Data) => void;\n\nexport class EventEmitter<Events extends EventMap> {\n #listenerMap: {\n [Key in keyof Events]?: EventListener<Events[Key]>[];\n } = {};\n\n addListener<Type extends keyof Events>(\n type: Type,\n listener: EventListener<Events[Type]>\n ) {\n const listeners = this.#listenerMap[type];\n if (listeners === undefined) {\n this.#listenerMap[type] = [listener];\n } else {\n if (!listeners.includes(listener)) {\n listeners.push(listener);\n }\n }\n\n return () => {\n this.removeListener(type, listener);\n };\n }\n\n emit<Type extends keyof Events>(type: Type, data: Events[Type]) {\n const listeners = this.#listenerMap[type];\n if (listeners !== undefined) {\n if (listeners.length === 1) {\n const listener = listeners[0];\n listener.call(null, data);\n } else {\n let didThrow = false;\n let caughtError = null;\n\n // Clone the current listeners before calling\n // in case calling triggers listeners to be added or removed\n const clonedListeners = Array.from(listeners);\n for (let i = 0; i < clonedListeners.length; i++) {\n const listener = clonedListeners[i];\n try {\n listener.call(null, data);\n } catch (error) {\n if (caughtError === null) {\n didThrow = true;\n caughtError = error;\n }\n }\n }\n\n if (didThrow) {\n throw caughtError;\n }\n }\n }\n }\n\n removeAllListeners() {\n this.#listenerMap = {};\n }\n\n removeListener<Type extends keyof Events>(\n type: Type,\n listener: EventListener<Events[Type]>\n ) {\n const listeners = this.#listenerMap[type];\n if (listeners !== undefined) {\n const index = listeners.indexOf(listener);\n if (index >= 0) {\n listeners.splice(index, 1);\n }\n }\n }\n}\n","import type { Layout, RegisteredGroup } from \"../../components/group/types\";\nimport type { PanelConstraints } from \"../../components/panel/types\";\nimport { EventEmitter } from \"../../utils/EventEmitter\";\nimport type { SeparatorToPanelsMap } from \"./types\";\n\ntype State = {\n defaultLayoutDeferred: boolean;\n derivedPanelConstraints: PanelConstraints[];\n groupSize: number;\n layout: Layout;\n separatorToPanels: SeparatorToPanelsMap;\n};\n\nexport type MountedGroups = Map<RegisteredGroup, State>;\n\nlet map: MountedGroups = new Map();\n\ntype GroupChangeEvent = {\n group: RegisteredGroup;\n next: State;\n prev: State | undefined;\n};\ntype GroupsChangeEvent = {\n next: MountedGroups;\n prev: MountedGroups;\n};\n\nconst eventEmitter = new EventEmitter<{\n groupChange: GroupChangeEvent;\n groupsChange: GroupsChangeEvent;\n}>();\n\nexport function deleteMutableGroup(group: RegisteredGroup) {\n map = new Map(map);\n map.delete(group);\n}\n\nexport function getRegisteredGroup(\n groupId: string\n): RegisteredGroup | undefined;\nexport function getRegisteredGroup(\n groupId: string,\n assert: true\n): RegisteredGroup;\nexport function getRegisteredGroup(groupId: string, assert?: boolean) {\n for (const [group] of map) {\n if (group.id === groupId) {\n return group;\n }\n }\n\n if (assert) {\n throw Error(`Could not find data for Group with id ${groupId}`);\n }\n\n return undefined;\n}\n\nexport function getMountedGroupState(groupId: string): State | undefined;\nexport function getMountedGroupState(groupId: string, assert: true): State;\nexport function getMountedGroupState(groupId: string, assert?: boolean) {\n for (const [group, mountedGroup] of map) {\n if (group.id === groupId) {\n return mountedGroup;\n }\n }\n\n if (assert) {\n throw Error(`Could not find data for Group with id ${groupId}`);\n }\n\n return undefined;\n}\n\nexport function getMountedGroups() {\n return map;\n}\n\nexport function subscribeToMountedGroup(\n groupId: string,\n callback: (event: GroupChangeEvent) => void\n) {\n return eventEmitter.addListener(\"groupChange\", (event) => {\n if (event.group.id === groupId) {\n callback(event);\n }\n });\n}\n\nexport function updateMountedGroup(group: RegisteredGroup, next: State) {\n const prev = map.get(group);\n\n map = new Map(map);\n map.set(group, next);\n\n eventEmitter.emit(\"groupChange\", {\n group,\n prev,\n next\n });\n}\n","import type { Orientation } from \"../../components/group/types\";\nimport type { Point } from \"../../types\";\nimport type { HitRegion } from \"../dom/calculateHitRegions\";\nimport { getDistanceBetweenPointAndRect } from \"./getDistanceBetweenPointAndRect\";\n\nexport function findClosestHitRegion(\n orientation: Orientation,\n hitRegions: HitRegion[],\n point: Point\n) {\n let closestHitRegion: HitRegion | undefined = undefined;\n let minDistance = {\n x: Infinity,\n y: Infinity\n };\n\n for (const hitRegion of hitRegions) {\n const data = getDistanceBetweenPointAndRect(point, hitRegion.rect);\n switch (orientation) {\n case \"horizontal\": {\n if (data.x <= minDistance.x) {\n closestHitRegion = hitRegion;\n minDistance = data;\n }\n break;\n }\n case \"vertical\": {\n if (data.y <= minDistance.y) {\n closestHitRegion = hitRegion;\n minDistance = data;\n }\n break;\n }\n }\n }\n\n return closestHitRegion\n ? {\n distance: minDistance,\n hitRegion: closestHitRegion\n }\n : undefined;\n}\n","// Detects ShadowRoot without requiring instanceof and browser globals\nexport function isShadowRoot(value: unknown): value is ShadowRoot {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"nodeType\" in value &&\n value.nodeType === Node.DOCUMENT_FRAGMENT_NODE\n );\n}\n","// Forked from NPM stacking-order@2.0.0\n// - github.com/Rich-Harris/stacking-order/issues/3\n// - github.com/Rich-Harris/stacking-order/issues/6\n\nimport { assert } from \"../utils/assert\";\nimport { isShadowRoot } from \"../utils/isShadowRoot\";\n\n/**\n * Determine which of two nodes appears in front of the other —\n * if `a` is in front, returns 1, otherwise returns -1\n * @param {HTMLElement | SVGElement} a\n * @param {HTMLElement | SVGElement} b\n */\nexport function compare(\n a: HTMLElement | SVGElement,\n b: HTMLElement | SVGElement\n): number {\n if (a === b) throw new Error(\"Cannot compare node with itself\");\n\n const ancestors = {\n a: get_ancestors(a),\n b: get_ancestors(b)\n };\n\n let common_ancestor;\n\n // remove shared ancestors\n while (ancestors.a.at(-1) === ancestors.b.at(-1)) {\n common_ancestor = ancestors.a.pop() as HTMLElement;\n ancestors.b.pop();\n }\n\n assert(\n common_ancestor,\n \"Stacking order can only be calculated for elements with a common ancestor\"\n );\n\n const z_indexes = {\n a: get_z_index(find_stacking_context(ancestors.a)),\n b: get_z_index(find_stacking_context(ancestors.b))\n };\n\n if (z_indexes.a === z_indexes.b) {\n const children = common_ancestor.childNodes;\n\n const furthest_ancestors = {\n a: ancestors.a.at(-1),\n b: ancestors.b.at(-1)\n };\n\n let i = children.length;\n while (i--) {\n const child = children[i];\n if (child === furthest_ancestors.a) return 1;\n if (child === furthest_ancestors.b) return -1;\n }\n }\n\n return Math.sign(z_indexes.a - z_indexes.b);\n}\n\nconst props =\n /\\b(?:position|zIndex|opacity|transform|webkitTransform|mixBlendMode|filter|webkitFilter|isolation)\\b/;\n\n/** @param {HTMLElement | SVGElement} node */\nfunction is_flex_item(node: HTMLElement | SVGElement) {\n // @ts-expect-error ParentNode vs Element\n const display = getComputedStyle(get_parent(node) ?? node).display;\n return display === \"flex\" || display === \"inline-flex\";\n}\n\n/** @param {HTMLElement | SVGElement} node */\nfunction creates_stacking_context(node: HTMLElement | SVGElement) {\n const style = getComputedStyle(node);\n\n // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context\n if (style.position === \"fixed\") return true;\n // Forked to fix upstream bug https://github.com/Rich-Harris/stacking-order/issues/3\n // if (\n // (style.zIndex !== \"auto\" && style.position !== \"static\") ||\n // is_flex_item(node)\n // )\n if (\n style.zIndex !== \"auto\" &&\n (style.position !== \"static\" || is_flex_item(node))\n )\n return true;\n if (+style.opacity < 1) return true;\n if (\"transform\" in style && style.transform !== \"none\") return true;\n if (\"webkitTransform\" in style && style.webkitTransform !== \"none\")\n return true;\n if (\"mixBlendMode\" in style && style.mixBlendMode !== \"normal\") return true;\n if (\"filter\" in style && style.filter !== \"none\") return true;\n if (\"webkitFilter\" in style && style.webkitFilter !== \"none\") return true;\n if (\"isolation\" in style && style.isolation === \"isolate\") return true;\n if (props.test(style.willChange)) return true;\n // @ts-expect-error Unrecognized prop\n if (style.webkitOverflowScrolling === \"touch\") return true;\n\n return false;\n}\n\n/** @param {(HTMLElement| SVGElement)[]} nodes */\nfunction find_stacking_context(nodes: (HTMLElement | SVGElement)[]) {\n let i = nodes.length;\n\n while (i--) {\n const node = nodes[i];\n assert(node, \"Missing node\");\n if (creates_stacking_context(node)) return node;\n }\n\n return null;\n}\n\n/** @param {HTMLElement | SVGElement} node */\nfunction get_z_index(node: HTMLElement | SVGElement | null) {\n return (node && Number(getComputedStyle(node).zIndex)) || 0;\n}\n\n/** @param {HTMLElement} node */\nfunction get_ancestors(node: HTMLElement | SVGElement | null) {\n const ancestors = [];\n\n while (node) {\n ancestors.push(node);\n // @ts-expect-error ParentNode vs Element\n node = get_parent(node);\n }\n\n return ancestors; // [ node, ... <body>, <html>, document ]\n}\n\n/** @param {HTMLElement} node */\nfunction get_parent(node: HTMLElement) {\n const { parentNode } = node;\n if (isShadowRoot(parentNode)) {\n return parentNode.host;\n }\n return parentNode;\n}\n","import type { Rect } from \"../../types\";\n\nexport function doRectsIntersect(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n","import { isHTMLElement } from \"../../utils/isHTMLElement\";\nimport { compare } from \"../../vendor/stacking-order\";\nimport { doRectsIntersect } from \"./doRectsIntersect\";\n\n// This library adds pointer event handlers to the Window for two reasons:\n// 1. It allows detecting when the pointer is \"near\" to a panel border or separator element,\n// (which can be particularly helpful on touch devices)\n// 2. It allows detecting pointer interactions that apply to multiple, nearby panels/separators\n// (in the event of e.g. nested groups)\n//\n// Because events are handled at the Window, it's important to detect when another element is \"above\" a separator (e.g. a modal)\n// as this should prevent the separator element from being clicked.\n// This function does that determination.\nexport function isViableHitTarget({\n groupElement,\n hitRegion,\n pointerEventTarget\n}: {\n groupElement: HTMLElement;\n hitRegion: DOMRect;\n pointerEventTarget: EventTarget | null;\n}) {\n if (\n !isHTMLElement(pointerEventTarget) ||\n pointerEventTarget.contains(groupElement) ||\n groupElement.contains(pointerEventTarget)\n ) {\n // Calculating stacking order has a cost;\n // If either group or element contain the other, the click is safe and we can skip calculating the indices\n return true;\n }\n\n if (compare(pointerEventTarget, groupElement) > 0) {\n // If the pointer target is above the separator, check for overlap\n // If they are near each other, but not overlapping, then the separator is still a viable target\n //\n // Note that it's not sufficient to compare only the target\n // The target might be a small element inside of a larger container\n // (For example, a SPAN or a DIV inside of a larger modal dialog)\n let currentElement: HTMLElement | SVGElement | null = pointerEventTarget;\n while (currentElement) {\n if (currentElement.contains(groupElement)) {\n return true;\n } else if (\n doRectsIntersect(currentElement.getBoundingClientRect(), hitRegion)\n ) {\n return false;\n }\n\n currentElement = currentElement.parentElement;\n }\n }\n\n return true;\n}\n","import {\n calculateHitRegions,\n type HitRegion\n} from \"../dom/calculateHitRegions\";\nimport type { MountedGroups } from \"../mutable-state/groups\";\nimport { findClosestHitRegion } from \"./findClosestHitRegion\";\nimport { isViableHitTarget } from \"./isViableHitTarget\";\n\nexport function findMatchingHitRegions(\n event: {\n clientX: number;\n clientY: number;\n target: EventTarget | null;\n },\n mountedGroups: MountedGroups\n): HitRegion[] {\n const matchingHitRegions: HitRegion[] = [];\n\n mountedGroups.forEach((_, groupData) => {\n if (groupData.disabled) {\n return;\n }\n\n const hitRegions = calculateHitRegions(groupData);\n const match = findClosestHitRegion(groupData.orientation, hitRegions, {\n x: event.clientX,\n y: event.clientY\n });\n if (\n match &&\n match.distance.x <= 0 &&\n match.distance.y <= 0 &&\n isViableHitTarget({\n groupElement: groupData.element,\n hitRegion: match.hitRegion.rect,\n pointerEventTarget: event.target\n })\n ) {\n matchingHitRegions.push(match.hitRegion);\n }\n });\n\n return matchingHitRegions;\n}\n","export function isArrayEqual(a: number[], b: number[]) {\n if (a.length !== b.length) {\n return false;\n } else {\n for (let index = 0; index < a.length; index++) {\n if (a[index] != b[index]) {\n return false;\n }\n }\n }\n return true;\n}\n","import { formatLayoutNumber } from \"./formatLayoutNumber\";\n\nexport function layoutNumbersEqual(\n actual: number,\n expected: number,\n minimumDelta = 0\n) {\n return (\n Math.abs(formatLayoutNumber(actual) - formatLayoutNumber(expected)) <=\n minimumDelta\n );\n}\n","import { layoutNumbersEqual } from \"./layoutNumbersEqual\";\n\nexport function compareLayoutNumbers(actual: number, expected: number) {\n if (layoutNumbersEqual(actual, expected)) {\n return 0;\n } else {\n return actual > expected ? 1 : -1;\n }\n}\n","import type { PanelConstraints } from \"../../components/panel/types\";\nimport { compareLayoutNumbers } from \"./compareLayoutNumbers\";\nimport { formatLayoutNumber } from \"./formatLayoutNumber\";\n\n// Panel size must be in percentages; pixel values should be pre-converted\nexport function validatePanelSize({\n overrideDisabledPanels,\n panelConstraints,\n prevSize,\n size\n}: {\n overrideDisabledPanels?: boolean;\n panelConstraints: PanelConstraints;\n prevSize: number;\n size: number;\n}) {\n const {\n collapsedSize = 0,\n collapsible,\n disabled,\n maxSize = 100,\n minSize = 0\n } = panelConstraints;\n\n if (disabled && !overrideDisabledPanels) {\n return prevSize;\n }\n\n if (compareLayoutNumbers(size, minSize) < 0) {\n if (collapsible) {\n // Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size.\n const halfwayPoint = (collapsedSize + minSize) / 2;\n if (compareLayoutNumbers(size, halfwayPoint) < 0) {\n size = collapsedSize;\n } else {\n size = minSize;\n }\n } else {\n size = minSize;\n }\n }\n\n size = Math.min(maxSize, size);\n size = formatLayoutNumber(size);\n\n return size;\n}\n","import type { Layout } from \"../../components/group/types\";\nimport type { PanelConstraints } from \"../../components/panel/types\";\nimport { assert } from \"../../utils/assert\";\nimport { isArrayEqual } from \"../../utils/isArrayEqual\";\nimport { compareLayoutNumbers } from \"../utils/compareLayoutNumbers\";\nimport { layoutNumbersEqual } from \"../utils/layoutNumbersEqual\";\nimport { validatePanelSize } from \"../utils/validatePanelSize\";\n\n// All units must be in percentages; pixel values should be pre-converted\nexport function adjustLayoutByDelta({\n delta,\n initialLayout: initialLayoutProp,\n panelConstraints: panelConstraintsArray,\n pivotIndices,\n prevLayout: prevLayoutProp,\n trigger\n}: {\n delta: number;\n initialLayout: Layout;\n panelConstraints: PanelConstraints[];\n pivotIndices: number[];\n prevLayout: Layout;\n trigger?: \"imperative-api\" | \"keyboard\" | \"mouse-or-touch\";\n}): Layout {\n if (layoutNumbersEqual(delta, 0)) {\n return initialLayoutProp;\n }\n\n const overrideDisabledPanels = trigger === \"imperative-api\";\n\n const initialLayout = Object.values(initialLayoutProp);\n const prevLayout = Object.values(prevLayoutProp);\n const nextLayout = [...initialLayout];\n\n const [firstPivotIndex, secondPivotIndex] = pivotIndices;\n assert(firstPivotIndex != null, \"Invalid first pivot index\");\n assert(secondPivotIndex != null, \"Invalid second pivot index\");\n\n let deltaApplied = 0;\n\n // const DEBUG = [];\n // DEBUG.push(`adjustLayoutByDelta()`);\n // DEBUG.push(` initialLayout: ${initialLayout.join(\", \")}`);\n // DEBUG.push(` prevLayout: ${prevLayout.join(\", \")}`);\n // DEBUG.push(` delta: ${delta}`);\n // DEBUG.push(` pivotIndices: ${pivotIndices.join(\", \")}`);\n // DEBUG.push(` trigger: ${trigger}`);\n // DEBUG.push(\"\");\n\n // A resizing panel affects the panels before or after it.\n //\n // A negative delta means the panel(s) immediately after the separator should grow/expand by decreasing its offset.\n // Other panels may also need to shrink/contract (and shift) to make room, depending on the min weights.\n //\n // A positive delta means the panel(s) immediately before the separator should \"expand\".\n // This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the separator.\n\n {\n switch (trigger) {\n case \"keyboard\": {\n // If this is a resize triggered by a keyboard event, our logic for expanding/collapsing is different.\n // We no longer check the halfway threshold because this may prevent the panel from expanding at all.\n {\n // Check if we should expand a collapsed panel\n const index = delta < 0 ? secondPivotIndex : firstPivotIndex;\n const panelConstraints = panelConstraintsArray[index];\n assert(\n panelConstraints,\n `Panel constraints not found for index ${index}`\n );\n\n const {\n collapsedSize = 0,\n collapsible,\n minSize = 0\n } = panelConstraints;\n\n // DEBUG.push(`edge case check 1: ${index}`);\n // DEBUG.push(` -> collapsible? ${collapsible}`);\n if (collapsible) {\n const prevSize = initialLayout[index];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${index}`\n );\n\n if (layoutNumbersEqual(prevSize, collapsedSize)) {\n const localDelta = minSize - prevSize;\n // DEBUG.push(` -> expand delta: ${localDelta}`);\n\n if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {\n delta = delta < 0 ? 0 - localDelta : localDelta;\n // DEBUG.push(` -> delta: ${delta}`);\n }\n }\n }\n }\n\n {\n // Check if we should collapse a panel at its minimum size\n const index = delta < 0 ? firstPivotIndex : secondPivotIndex;\n const panelConstraints = panelConstraintsArray[index];\n assert(\n panelConstraints,\n `No panel constraints found for index ${index}`\n );\n\n const {\n collapsedSize = 0,\n collapsible,\n minSize = 0\n } = panelConstraints;\n\n // DEBUG.push(`edge case check 2: ${index}`);\n // DEBUG.push(` -> collapsible? ${collapsible}`);\n if (collapsible) {\n const prevSize = initialLayout[index];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${index}`\n );\n\n if (layoutNumbersEqual(prevSize, minSize)) {\n const localDelta = prevSize - collapsedSize;\n // DEBUG.push(` -> expand delta: ${localDelta}`);\n\n if (compareLayoutNumbers(localDelta, Math.abs(delta)) > 0) {\n delta = delta < 0 ? 0 - localDelta : localDelta;\n // DEBUG.push(` -> delta: ${delta}`);\n }\n }\n }\n }\n break;\n }\n default: {\n // If we're starting from a collapsed state, dragging past the halfway point should cause the panel to expand\n // This can happen for positive or negative drags, and panels on either side of the separator can be collapsible\n // The easiest way to support this is to detect this scenario and pre-adjust the delta before applying the rest of the layout algorithm\n // DEBUG.push(`edge case check 3: collapsible panels`);\n\n const index = delta < 0 ? secondPivotIndex : firstPivotIndex;\n const panelConstraints = panelConstraintsArray[index];\n assert(\n panelConstraints,\n `Panel constraints not found for index ${index}`\n );\n\n const prevSize = initialLayout[index];\n\n const { collapsible, collapsedSize, minSize } = panelConstraints;\n if (collapsible && compareLayoutNumbers(prevSize, minSize) < 0) {\n // DEBUG.push(` -> collapsible ${delta < 0 ? \"2nd\" : \"1st\"} panel`);\n if (delta > 0) {\n const gapSize = minSize - collapsedSize;\n const halfwayDelta = gapSize / 2;\n // DEBUG.push(` -> halfway delta: ${halfwayDelta}`);\n // DEBUG.push(` collapsed: ${collapsedSize}`);\n // DEBUG.push(` min: ${minSize}`);\n\n const nextSize = prevSize + delta;\n if (compareLayoutNumbers(nextSize, minSize) < 0) {\n // DEBUG.push(\" -> adjusting delta\");\n // DEBUG.push(` from: ${delta}`);\n delta =\n compareLayoutNumbers(delta, halfwayDelta) <= 0 ? 0 : gapSize;\n // DEBUG.push(` to: ${delta}`);\n }\n } else {\n const gapSize = minSize - collapsedSize;\n const halfwayDelta = 100 - gapSize / 2;\n // DEBUG.push(` -> halfway delta: ${halfwayDelta}`);\n // DEBUG.push(` collapsed: ${100 - collapsedSize}`);\n // DEBUG.push(` min: ${100 - minSize}`);\n\n const nextSize = prevSize - delta;\n if (compareLayoutNumbers(nextSize, minSize) < 0) {\n // DEBUG.push(\" -> adjusting delta\");\n // DEBUG.push(` from: ${delta}`);\n delta =\n compareLayoutNumbers(100 + delta, halfwayDelta) > 0\n ? 0\n : -gapSize;\n // DEBUG.push(` to: ${delta}`);\n }\n }\n }\n break;\n }\n }\n // DEBUG.push(\"\");\n }\n\n {\n // Pre-calculate max available delta in the opposite direction of our pivot.\n // This will be the maximum amount we're allowed to expand/contract the panels in the primary direction.\n // If this amount is less than the requested delta, adjust the requested delta.\n // If this amount is greater than the requested delta, that's useful information too–\n // as an expanding panel might change from collapsed to min size.\n\n const increment = delta < 0 ? 1 : -1;\n\n let index = delta < 0 ? secondPivotIndex : firstPivotIndex;\n let maxAvailableDelta = 0;\n\n // DEBUG.push(\"pre calc...\");\n while (true) {\n const prevSize = initialLayout[index];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${index}`\n );\n\n const maxSafeSize = validatePanelSize({\n overrideDisabledPanels,\n panelConstraints: panelConstraintsArray[index],\n prevSize,\n size: 100\n });\n const delta = maxSafeSize - prevSize;\n // DEBUG.push(` ${index}: ${prevSize} -> ${maxSafeSize}`);\n\n maxAvailableDelta += delta;\n index += increment;\n\n if (index < 0 || index >= panelConstraintsArray.length) {\n break;\n }\n }\n\n // DEBUG.push(` -> max available delta: ${maxAvailableDelta}`);\n const minAbsDelta = Math.min(Math.abs(delta), Math.abs(maxAvailableDelta));\n delta = delta < 0 ? 0 - minAbsDelta : minAbsDelta;\n // DEBUG.push(` -> adjusted delta: ${delta}`);\n // DEBUG.push(\"\");\n }\n\n {\n // Delta added to a panel needs to be subtracted from other panels (within the constraints that those panels allow).\n\n const pivotIndex = delta < 0 ? firstPivotIndex : secondPivotIndex;\n let index = pivotIndex;\n while (index >= 0 && index < panelConstraintsArray.length) {\n const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);\n\n const prevSize = initialLayout[index];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${index}`\n );\n\n const unsafeSize = prevSize - deltaRemaining;\n const safeSize = validatePanelSize({\n overrideDisabledPanels,\n panelConstraints: panelConstraintsArray[index],\n prevSize,\n size: unsafeSize\n });\n\n if (!layoutNumbersEqual(prevSize, safeSize)) {\n deltaApplied += prevSize - safeSize;\n\n nextLayout[index] = safeSize;\n\n if (\n deltaApplied\n .toFixed(3)\n .localeCompare(Math.abs(delta).toFixed(3), undefined, {\n numeric: true\n }) >= 0\n ) {\n break;\n }\n }\n\n if (delta < 0) {\n index--;\n } else {\n index++;\n }\n }\n }\n // DEBUG.push(`after 1: ${nextLayout.join(\", \")}`);\n // DEBUG.push(` deltaApplied: ${deltaApplied}`);\n // DEBUG.push(\"\");\n\n // If we were unable to resize any of the panels panels, return the previous state.\n // This will essentially bailout and ignore e.g. drags past a panel's boundaries\n if (isArrayEqual(prevLayout, nextLayout)) {\n // DEBUG.push(`bailout to previous layout: ${prevLayout.join(\", \")}`);\n // console.log(DEBUG.join(\"\\n\"));\n\n return prevLayoutProp;\n }\n\n {\n // Now distribute the applied delta to the panels in the other direction\n const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex;\n\n const prevSize = initialLayout[pivotIndex];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${pivotIndex}`\n );\n\n const unsafeSize = prevSize + deltaApplied;\n const safeSize = validatePanelSize({\n overrideDisabledPanels,\n panelConstraints: panelConstraintsArray[pivotIndex],\n prevSize,\n size: unsafeSize\n });\n\n // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.\n nextLayout[pivotIndex] = safeSize;\n\n // Edge case where expanding or contracting one panel caused another one to change collapsed state\n if (!layoutNumbersEqual(safeSize, unsafeSize)) {\n let deltaRemaining = unsafeSize - safeSize;\n\n const pivotIndex = delta < 0 ? secondPivotIndex : firstPivotIndex;\n let index = pivotIndex;\n while (index >= 0 && index < panelConstraintsArray.length) {\n const prevSize = nextLayout[index];\n assert(\n prevSize != null,\n `Previous layout not found for panel index ${index}`\n );\n\n const unsafeSize = prevSize + deltaRemaining;\n const safeSize = validatePanelSize({\n overrideDisabledPanels,\n panelConstraints: panelConstraintsArray[index],\n prevSize,\n size: unsafeSize\n });\n\n if (!layoutNumbersEqual(prevSize, safeSize)) {\n deltaRemaining -= safeSize - prevSize;\n\n nextLayout[index] = safeSize;\n