@octanejs/resizable-panels 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE.upstream +21 -0
  2. package/README.md +22 -0
  3. package/UPSTREAM.md +64 -0
  4. package/package.json +61 -0
  5. package/src/components/group/Group.tsrx +244 -0
  6. package/src/components/group/GroupContext.ts +25 -0
  7. package/src/components/group/auto-save/getStorageKey.ts +3 -0
  8. package/src/components/group/auto-save/readLegacyLayout.ts +37 -0
  9. package/src/components/group/sortByElementOffset.ts +20 -0
  10. package/src/components/group/types.ts +50 -0
  11. package/src/components/group/useDefaultLayout.ts +152 -0
  12. package/src/components/group/useGroupCallbackRef.ts +6 -0
  13. package/src/components/group/useGroupContext.ts +12 -0
  14. package/src/components/group/useGroupImperativeHandle.ts +46 -0
  15. package/src/components/group/useGroupRef.ts +6 -0
  16. package/src/components/panel/Panel.tsrx +130 -0
  17. package/src/components/panel/types.ts +66 -0
  18. package/src/components/panel/usePanelCallbackRef.ts +6 -0
  19. package/src/components/panel/usePanelImperativeHandle.ts +42 -0
  20. package/src/components/panel/usePanelRef.ts +6 -0
  21. package/src/components/separator/Separator.tsrx +126 -0
  22. package/src/components/separator/types.ts +22 -0
  23. package/src/constants.ts +25 -0
  24. package/src/global/cursor/getCursorStyle.ts +102 -0
  25. package/src/global/cursor/supportsAdvancedCursorStyles.ts +25 -0
  26. package/src/global/cursor/updateCursorStyle.ts +77 -0
  27. package/src/global/dom/calculateAvailableGroupSize.ts +11 -0
  28. package/src/global/dom/calculateHitRegions.ts +200 -0
  29. package/src/global/dom/calculatePanelConstraints.ts +84 -0
  30. package/src/global/event-handlers/onDocumentContextMenu.ts +9 -0
  31. package/src/global/event-handlers/onDocumentDoubleClick.ts +31 -0
  32. package/src/global/event-handlers/onDocumentKeyDown.ts +133 -0
  33. package/src/global/event-handlers/onDocumentPointerDown.ts +56 -0
  34. package/src/global/event-handlers/onDocumentPointerLeave.ts +21 -0
  35. package/src/global/event-handlers/onDocumentPointerMove.ts +89 -0
  36. package/src/global/event-handlers/onDocumentPointerOut.ts +19 -0
  37. package/src/global/event-handlers/onDocumentPointerUp.ts +14 -0
  38. package/src/global/mountGroup.ts +213 -0
  39. package/src/global/mutable-state/groups.ts +104 -0
  40. package/src/global/mutable-state/interactions.ts +49 -0
  41. package/src/global/mutable-state/types.ts +31 -0
  42. package/src/global/styles/convertEmToPixels.ts +6 -0
  43. package/src/global/styles/convertRemToPixels.ts +6 -0
  44. package/src/global/styles/convertVhToPixels.ts +3 -0
  45. package/src/global/styles/convertVwToPixels.ts +3 -0
  46. package/src/global/styles/parseSizeAndUnit.ts +28 -0
  47. package/src/global/styles/sizeStyleToPixels.ts +48 -0
  48. package/src/global/utils/adjustLayoutByDelta.ts +339 -0
  49. package/src/global/utils/adjustLayoutForSeparator.ts +54 -0
  50. package/src/global/utils/calculateDefaultLayout.ts +37 -0
  51. package/src/global/utils/calculateSeparatorAriaValues.ts +67 -0
  52. package/src/global/utils/compareLayoutNumbers.ts +9 -0
  53. package/src/global/utils/completeActivePointerResize.ts +37 -0
  54. package/src/global/utils/doRectsIntersect.ts +5 -0
  55. package/src/global/utils/findClosestHitRegion.ts +43 -0
  56. package/src/global/utils/findClosestRect.ts +36 -0
  57. package/src/global/utils/findMatchingHitRegions.ts +41 -0
  58. package/src/global/utils/findSeparatorGroup.ts +13 -0
  59. package/src/global/utils/formatLayoutNumber.ts +3 -0
  60. package/src/global/utils/getDistanceBetweenPointAndRect.ts +14 -0
  61. package/src/global/utils/getImperativeGroupMethods.ts +68 -0
  62. package/src/global/utils/getImperativePanelMethods.ts +228 -0
  63. package/src/global/utils/isCoarsePointer.ts +8 -0
  64. package/src/global/utils/isViableHitTarget.ts +53 -0
  65. package/src/global/utils/layoutNumbersEqual.ts +5 -0
  66. package/src/global/utils/layoutsEqual.ts +17 -0
  67. package/src/global/utils/notifyPanelOnResize.ts +33 -0
  68. package/src/global/utils/objectsEqual.ts +14 -0
  69. package/src/global/utils/preserveFixedPanelSizes.ts +74 -0
  70. package/src/global/utils/updateActiveHitRegion.ts +132 -0
  71. package/src/global/utils/validateLayoutKeys.ts +19 -0
  72. package/src/global/utils/validatePanelGroupLayout.ts +92 -0
  73. package/src/global/utils/validatePanelSize.ts +41 -0
  74. package/src/hooks/useForceUpdate.ts +8 -0
  75. package/src/hooks/useId.ts +6 -0
  76. package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
  77. package/src/hooks/useMergedRefs.ts +18 -0
  78. package/src/hooks/useStableArray.ts +15 -0
  79. package/src/hooks/useStableCallback.ts +18 -0
  80. package/src/hooks/useStableObject.ts +15 -0
  81. package/src/index.tsrx +12 -0
  82. package/src/internal.ts +23 -0
  83. package/src/types.ts +18 -0
  84. package/src/utils/EventEmitter.ts +72 -0
  85. package/src/utils/assert.ts +8 -0
  86. package/src/utils/isArrayEqual.ts +12 -0
  87. package/src/utils/isHTMLElement.ts +9 -0
  88. package/src/utils/isShadowRoot.ts +9 -0
  89. package/src/utils/parseNumericStyleValue.ts +15 -0
  90. package/src/vendor/stacking-order.ts +133 -0
  91. package/status.json +11 -0
@@ -0,0 +1,41 @@
1
+ import { calculateHitRegions, type HitRegion } from '../dom/calculateHitRegions';
2
+ import type { MountedGroups } from '../mutable-state/groups';
3
+ import { findClosestHitRegion } from './findClosestHitRegion';
4
+ import { isViableHitTarget } from './isViableHitTarget';
5
+
6
+ export function findMatchingHitRegions(
7
+ event: {
8
+ clientX: number;
9
+ clientY: number;
10
+ target: EventTarget | null;
11
+ },
12
+ mountedGroups: MountedGroups,
13
+ ): HitRegion[] {
14
+ const matchingHitRegions: HitRegion[] = [];
15
+
16
+ mountedGroups.forEach((_, groupData) => {
17
+ if (groupData.disabled) {
18
+ return;
19
+ }
20
+
21
+ const hitRegions = calculateHitRegions(groupData);
22
+ const match = findClosestHitRegion(groupData.orientation, hitRegions, {
23
+ x: event.clientX,
24
+ y: event.clientY,
25
+ });
26
+ if (
27
+ match &&
28
+ match.distance.x <= 0 &&
29
+ match.distance.y <= 0 &&
30
+ isViableHitTarget({
31
+ groupElement: groupData.element,
32
+ hitRegion: match.hitRegion.rect,
33
+ pointerEventTarget: event.target,
34
+ })
35
+ ) {
36
+ matchingHitRegions.push(match.hitRegion);
37
+ }
38
+ });
39
+
40
+ return matchingHitRegions;
41
+ }
@@ -0,0 +1,13 @@
1
+ import { getMountedGroups } from '../mutable-state/groups';
2
+
3
+ export function findSeparatorGroup(separatorElement: HTMLElement) {
4
+ const mountedGroups = getMountedGroups();
5
+
6
+ for (const [group] of mountedGroups) {
7
+ if (group.separators.some((separator) => separator.element === separatorElement)) {
8
+ return group;
9
+ }
10
+ }
11
+
12
+ throw Error('Could not find parent Group for separator element');
13
+ }
@@ -0,0 +1,3 @@
1
+ export function formatLayoutNumber(number: number) {
2
+ return parseFloat(number.toFixed(3));
3
+ }
@@ -0,0 +1,14 @@
1
+ import type { Point } from '../../types';
2
+
3
+ export function getDistanceBetweenPointAndRect(point: Point, rect: DOMRectReadOnly) {
4
+ return {
5
+ x:
6
+ point.x >= rect.left && point.x <= rect.right
7
+ ? 0
8
+ : Math.min(Math.abs(point.x - rect.left), Math.abs(point.x - rect.right)),
9
+ y:
10
+ point.y >= rect.top && point.y <= rect.bottom
11
+ ? 0
12
+ : Math.min(Math.abs(point.y - rect.top), Math.abs(point.y - rect.bottom)),
13
+ };
14
+ }
@@ -0,0 +1,68 @@
1
+ import type { GroupImperativeHandle, Layout } from '../../components/group/types';
2
+ import { getMountedGroups, updateMountedGroup } from '../mutable-state/groups';
3
+ import { layoutsEqual } from './layoutsEqual';
4
+ import { validatePanelGroupLayout } from './validatePanelGroupLayout';
5
+
6
+ export function getImperativeGroupMethods({ groupId }: { groupId: string }): GroupImperativeHandle {
7
+ const find = () => {
8
+ const mountedGroups = getMountedGroups();
9
+ for (const [group, value] of mountedGroups) {
10
+ if (group.id === groupId) {
11
+ return { group, ...value };
12
+ }
13
+ }
14
+
15
+ throw Error(`Could not find Group with id "${groupId}"`);
16
+ };
17
+
18
+ return {
19
+ getLayout() {
20
+ const { defaultLayoutDeferred, layout } = find();
21
+
22
+ if (defaultLayoutDeferred) {
23
+ // This indicates that the Group has not finished mounting yet
24
+ // Likely because it has been rendered inside of a hidden DOM subtree
25
+ // Any layout value will not have been validated and so it should not be returned
26
+ return {};
27
+ }
28
+
29
+ return layout;
30
+ },
31
+ setLayout(unsafeLayout: Layout) {
32
+ const {
33
+ defaultLayoutDeferred,
34
+ derivedPanelConstraints,
35
+ group,
36
+ groupSize,
37
+ layout: prevLayout,
38
+ separatorToPanels,
39
+ } = find();
40
+
41
+ const nextLayout = validatePanelGroupLayout({
42
+ layout: unsafeLayout,
43
+ panelConstraints: derivedPanelConstraints,
44
+ });
45
+
46
+ if (defaultLayoutDeferred) {
47
+ // This indicates that the Group has not finished mounting yet
48
+ // Likely because it has been rendered inside of a hidden DOM subtree
49
+ // In this case we cannot fully validate the layout, so we shouldn't apply it
50
+ // It's okay to run the validate function above though,
51
+ // it will still warn about certain types of errors (e.g. wrong number of panels)
52
+ return prevLayout;
53
+ }
54
+
55
+ if (!layoutsEqual(prevLayout, nextLayout)) {
56
+ updateMountedGroup(group, {
57
+ defaultLayoutDeferred,
58
+ derivedPanelConstraints,
59
+ groupSize,
60
+ layout: nextLayout,
61
+ separatorToPanels,
62
+ });
63
+ }
64
+
65
+ return nextLayout;
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,228 @@
1
+ import type { Layout } from '../../components/group/types';
2
+ import type {
3
+ PanelConstraints,
4
+ PanelImperativeHandle,
5
+ RegisteredPanel,
6
+ } from '../../components/panel/types';
7
+ import { calculateAvailableGroupSize } from '../dom/calculateAvailableGroupSize';
8
+ import { getMountedGroups, updateMountedGroup } from '../mutable-state/groups';
9
+ import { sizeStyleToPixels } from '../styles/sizeStyleToPixels';
10
+ import { adjustLayoutByDelta } from './adjustLayoutByDelta';
11
+ import { formatLayoutNumber } from './formatLayoutNumber';
12
+ import { layoutNumbersEqual } from './layoutNumbersEqual';
13
+ import { layoutsEqual } from './layoutsEqual';
14
+ import { validatePanelGroupLayout } from './validatePanelGroupLayout';
15
+
16
+ export function getImperativePanelMethods({
17
+ groupId,
18
+ panelId,
19
+ }: {
20
+ groupId: string;
21
+ panelId: string;
22
+ }): PanelImperativeHandle {
23
+ const find = () => {
24
+ const mountedGroups = getMountedGroups();
25
+ for (const [
26
+ group,
27
+ { defaultLayoutDeferred, derivedPanelConstraints, layout, groupSize, separatorToPanels },
28
+ ] of mountedGroups) {
29
+ if (group.id === groupId) {
30
+ return {
31
+ defaultLayoutDeferred,
32
+ derivedPanelConstraints,
33
+ group,
34
+ groupSize,
35
+ layout,
36
+ separatorToPanels,
37
+ };
38
+ }
39
+ }
40
+
41
+ throw Error(`Group ${groupId} not found`);
42
+ };
43
+
44
+ const getPanelConstraints = () => {
45
+ const match = find().derivedPanelConstraints.find((current) => current.panelId === panelId);
46
+ if (match !== undefined) {
47
+ return match;
48
+ }
49
+
50
+ throw Error(`Panel constraints not found for Panel ${panelId}`);
51
+ };
52
+
53
+ const getPanel = () => {
54
+ const match = find().group.panels.find((current) => current.id === panelId);
55
+ if (match !== undefined) {
56
+ return match;
57
+ }
58
+
59
+ throw Error(`Layout not found for Panel ${panelId}`);
60
+ };
61
+
62
+ const getPanelSize = () => {
63
+ const match = find().layout[panelId];
64
+ if (match !== undefined) {
65
+ return match;
66
+ }
67
+
68
+ throw Error(`Layout not found for Panel ${panelId}`);
69
+ };
70
+
71
+ /**
72
+ * Compute the next (unvalidated) layout when resizing a panel imperatively.
73
+ *
74
+ * Handles two edge cases for the last panel:
75
+ * 1. Single panel in the group — no sibling exists to form valid pivot indices.
76
+ * 2. All preceding panels are already collapsed — the normal reversed-delta
77
+ * logic would cascade the freed space to the first panel. Instead the last
78
+ * panel keeps the remainder so it stays the largest.
79
+ */
80
+ const computeLayout = ({
81
+ nextSize,
82
+ panels,
83
+ prevLayout,
84
+ derivedPanelConstraints,
85
+ }: {
86
+ nextSize: number;
87
+ panels: RegisteredPanel[];
88
+ prevLayout: Layout;
89
+ derivedPanelConstraints: PanelConstraints[];
90
+ }): Layout => {
91
+ const prevSize = getPanelSize();
92
+
93
+ const index = panels.findIndex((current) => current.id === panelId);
94
+ const isFirstPanel = index === 0;
95
+ const isLastPanel = index === panels.length - 1;
96
+
97
+ const allPreviousCollapsed =
98
+ isLastPanel &&
99
+ nextSize < prevSize &&
100
+ (isFirstPanel ||
101
+ panels.slice(0, index).every((_panel, panelIndex) => {
102
+ const pc = derivedPanelConstraints[panelIndex];
103
+ return pc?.collapsible && layoutNumbersEqual(pc.collapsedSize, prevLayout[pc.panelId]);
104
+ }));
105
+
106
+ if (allPreviousCollapsed) {
107
+ const occupiedByPrevious = panels
108
+ .slice(0, index)
109
+ .reduce((total, panel) => total + prevLayout[panel.id], 0);
110
+ return {
111
+ ...prevLayout,
112
+ [panelId]: formatLayoutNumber(100 - occupiedByPrevious),
113
+ };
114
+ }
115
+
116
+ return adjustLayoutByDelta({
117
+ delta: isLastPanel ? prevSize - nextSize : nextSize - prevSize,
118
+ initialLayout: prevLayout,
119
+ panelConstraints: derivedPanelConstraints,
120
+ pivotIndices: isLastPanel ? [index - 1, index] : [index, index + 1],
121
+ prevLayout,
122
+ trigger: 'imperative-api',
123
+ });
124
+ };
125
+
126
+ const setPanelSize = (nextSize: number) => {
127
+ const prevSize = getPanelSize();
128
+ if (nextSize === prevSize) {
129
+ return;
130
+ }
131
+
132
+ const {
133
+ defaultLayoutDeferred,
134
+ derivedPanelConstraints,
135
+ group,
136
+ groupSize,
137
+ layout: prevLayout,
138
+ separatorToPanels,
139
+ } = find();
140
+
141
+ const unsafeLayout = computeLayout({
142
+ nextSize,
143
+ panels: group.panels,
144
+ prevLayout,
145
+ derivedPanelConstraints,
146
+ });
147
+
148
+ const nextLayout = validatePanelGroupLayout({
149
+ layout: unsafeLayout,
150
+ panelConstraints: derivedPanelConstraints,
151
+ });
152
+ if (!layoutsEqual(prevLayout, nextLayout)) {
153
+ updateMountedGroup(group, {
154
+ defaultLayoutDeferred,
155
+ derivedPanelConstraints,
156
+ groupSize,
157
+ layout: nextLayout,
158
+ separatorToPanels,
159
+ });
160
+ }
161
+ };
162
+
163
+ return {
164
+ collapse: () => {
165
+ const { collapsible, collapsedSize } = getPanelConstraints();
166
+ const { mutableValues } = getPanel();
167
+ const size = getPanelSize();
168
+
169
+ if (collapsible && size !== collapsedSize) {
170
+ // Store previous size in to restore if expand() is called
171
+ mutableValues.expandToSize = size;
172
+
173
+ setPanelSize(collapsedSize);
174
+ }
175
+ },
176
+ expand: () => {
177
+ const { collapsible, collapsedSize, minSize } = getPanelConstraints();
178
+ const { mutableValues } = getPanel();
179
+ const size = getPanelSize();
180
+
181
+ if (collapsible && size === collapsedSize) {
182
+ // Restore pre-collapse size, fallback to minSize
183
+ let nextSize = mutableValues.expandToSize ?? minSize;
184
+
185
+ // Edge case: if minSize is 0, pick something meaningful to expand the panel to
186
+ if (nextSize === 0) {
187
+ nextSize = 1;
188
+ }
189
+
190
+ setPanelSize(nextSize);
191
+ }
192
+ },
193
+ getSize: () => {
194
+ const { group } = find();
195
+ const asPercentage = getPanelSize();
196
+ const { element } = getPanel();
197
+
198
+ const inPixels =
199
+ group.orientation === 'horizontal' ? element.offsetWidth : element.offsetHeight;
200
+
201
+ return {
202
+ asPercentage,
203
+ inPixels,
204
+ };
205
+ },
206
+ isCollapsed: () => {
207
+ const { collapsible, collapsedSize } = getPanelConstraints();
208
+ const size = getPanelSize();
209
+
210
+ return collapsible && layoutNumbersEqual(collapsedSize, size);
211
+ },
212
+ resize: (size: number | string) => {
213
+ const { group } = find();
214
+ const { element } = getPanel();
215
+ const groupSize = calculateAvailableGroupSize({ group });
216
+
217
+ const asPixels = sizeStyleToPixels({
218
+ groupSize,
219
+ panelElement: element,
220
+ styleProp: size,
221
+ });
222
+
223
+ const asPercentage = formatLayoutNumber((asPixels / groupSize) * 100);
224
+
225
+ setPanelSize(asPercentage);
226
+ },
227
+ } satisfies PanelImperativeHandle;
228
+ }
@@ -0,0 +1,8 @@
1
+ let cached: boolean | undefined;
2
+
3
+ /** Caches the current primary-pointer precision without reading browser globals during SSR. */
4
+ export function isCoarsePointer(): boolean {
5
+ if (cached === undefined)
6
+ cached = typeof matchMedia === 'function' && matchMedia('(pointer:coarse)').matches;
7
+ return cached;
8
+ }
@@ -0,0 +1,53 @@
1
+ import { isHTMLElement } from '../../utils/isHTMLElement';
2
+ import { compare } from '../../vendor/stacking-order';
3
+ import { doRectsIntersect } from './doRectsIntersect';
4
+
5
+ // This library adds pointer event handlers to the Window for two reasons:
6
+ // 1. It allows detecting when the pointer is "near" to a panel border or separator element,
7
+ // (which can be particularly helpful on touch devices)
8
+ // 2. It allows detecting pointer interactions that apply to multiple, nearby panels/separators
9
+ // (in the event of e.g. nested groups)
10
+ //
11
+ // Because events are handled at the Window, it's important to detect when another element is "above" a separator (e.g. a modal)
12
+ // as this should prevent the separator element from being clicked.
13
+ // This function does that determination.
14
+ export function isViableHitTarget({
15
+ groupElement,
16
+ hitRegion,
17
+ pointerEventTarget,
18
+ }: {
19
+ groupElement: HTMLElement;
20
+ hitRegion: DOMRect;
21
+ pointerEventTarget: EventTarget | null;
22
+ }) {
23
+ if (
24
+ !isHTMLElement(pointerEventTarget) ||
25
+ pointerEventTarget.contains(groupElement) ||
26
+ groupElement.contains(pointerEventTarget)
27
+ ) {
28
+ // Calculating stacking order has a cost;
29
+ // If either group or element contain the other, the click is safe and we can skip calculating the indices
30
+ return true;
31
+ }
32
+
33
+ if (compare(pointerEventTarget, groupElement) > 0) {
34
+ // If the pointer target is above the separator, check for overlap
35
+ // If they are near each other, but not overlapping, then the separator is still a viable target
36
+ //
37
+ // Note that it's not sufficient to compare only the target
38
+ // The target might be a small element inside of a larger container
39
+ // (For example, a SPAN or a DIV inside of a larger modal dialog)
40
+ let currentElement: HTMLElement | SVGElement | null = pointerEventTarget;
41
+ while (currentElement) {
42
+ if (currentElement.contains(groupElement)) {
43
+ return true;
44
+ } else if (doRectsIntersect(currentElement.getBoundingClientRect(), hitRegion)) {
45
+ return false;
46
+ }
47
+
48
+ currentElement = currentElement.parentElement;
49
+ }
50
+ }
51
+
52
+ return true;
53
+ }
@@ -0,0 +1,5 @@
1
+ import { formatLayoutNumber } from './formatLayoutNumber';
2
+
3
+ export function layoutNumbersEqual(actual: number, expected: number, minimumDelta = 0) {
4
+ return Math.abs(formatLayoutNumber(actual) - formatLayoutNumber(expected)) <= minimumDelta;
5
+ }
@@ -0,0 +1,17 @@
1
+ import type { Layout } from '../../components/group/types';
2
+ import { compareLayoutNumbers } from './compareLayoutNumbers';
3
+
4
+ export function layoutsEqual(a: Layout, b: Layout): boolean {
5
+ if (Object.keys(a).length !== Object.keys(b).length) {
6
+ return false;
7
+ }
8
+
9
+ for (const id in a) {
10
+ // Edge case: Panel id has been changed
11
+ if (b[id] === undefined || compareLayoutNumbers(a[id], b[id]) !== 0) {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ return true;
17
+ }
@@ -0,0 +1,33 @@
1
+ import type { RegisteredGroup } from '../../components/group/types';
2
+ import { calculateAvailableGroupSize } from '../dom/calculateAvailableGroupSize';
3
+ import { formatLayoutNumber } from './formatLayoutNumber';
4
+
5
+ export function notifyPanelOnResize(
6
+ group: RegisteredGroup,
7
+ element: HTMLElement,
8
+ borderBoxSize: readonly ResizeObserverSize[],
9
+ ) {
10
+ const resizeObserverSize = borderBoxSize[0];
11
+ if (!resizeObserverSize) {
12
+ return;
13
+ }
14
+
15
+ const panel = group.panels.find((current) => current.element === element);
16
+ if (!panel || !panel.onResize) {
17
+ return;
18
+ }
19
+
20
+ const groupSize = calculateAvailableGroupSize({ group });
21
+
22
+ const panelSize =
23
+ group.orientation === 'horizontal' ? panel.element.offsetWidth : panel.element.offsetHeight;
24
+
25
+ const prevSize = panel.mutableValues.prevSize;
26
+ const nextSize = {
27
+ asPercentage: formatLayoutNumber((panelSize / groupSize) * 100),
28
+ inPixels: panelSize,
29
+ };
30
+ panel.mutableValues.prevSize = nextSize;
31
+
32
+ panel.onResize(nextSize, panel.id, prevSize);
33
+ }
@@ -0,0 +1,14 @@
1
+ export function objectsEqual(a: object, b: object) {
2
+ const keys = Object.keys(a);
3
+ if (keys.length !== Object.keys(b).length) {
4
+ return false;
5
+ }
6
+
7
+ for (const key in a) {
8
+ if (a[key as keyof typeof a] !== b[key as keyof typeof b]) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ return true;
14
+ }
@@ -0,0 +1,74 @@
1
+ import type { Layout, RegisteredGroup } from '../../components/group/types';
2
+ import { formatLayoutNumber } from './formatLayoutNumber';
3
+
4
+ export function preserveFixedPanelSizes({
5
+ group,
6
+ nextGroupSize,
7
+ prevGroupSize,
8
+ prevLayout,
9
+ }: {
10
+ group: RegisteredGroup;
11
+ nextGroupSize: number;
12
+ prevGroupSize: number;
13
+ prevLayout: Layout;
14
+ }) {
15
+ if (prevGroupSize <= 0 || nextGroupSize <= 0 || prevGroupSize === nextGroupSize) {
16
+ return prevLayout;
17
+ }
18
+
19
+ let fixedPanelsTotalSize = 0;
20
+ let flexiblePanelsTotalPrevSize = 0;
21
+ let hasPreservePixelSizePanels = false;
22
+
23
+ const fixedPanels = new Map<string, number>();
24
+ const flexiblePanelIds: string[] = [];
25
+
26
+ for (const panel of group.panels) {
27
+ const prevPanelSize = prevLayout[panel.id] ?? 0;
28
+ switch (panel.panelConstraints.groupResizeBehavior) {
29
+ case 'preserve-pixel-size': {
30
+ hasPreservePixelSizePanels = true;
31
+
32
+ const prevPanelSizeInPixels = (prevPanelSize / 100) * prevGroupSize;
33
+ const nextPanelSize = formatLayoutNumber((prevPanelSizeInPixels / nextGroupSize) * 100);
34
+
35
+ fixedPanels.set(panel.id, nextPanelSize);
36
+ fixedPanelsTotalSize += nextPanelSize;
37
+ break;
38
+ }
39
+ case 'preserve-relative-size':
40
+ default: {
41
+ flexiblePanelIds.push(panel.id);
42
+ flexiblePanelsTotalPrevSize += prevPanelSize;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+
48
+ if (!hasPreservePixelSizePanels || flexiblePanelIds.length === 0) {
49
+ return prevLayout;
50
+ }
51
+
52
+ const remainingSize = 100 - fixedPanelsTotalSize;
53
+ const nextLayout = { ...prevLayout };
54
+
55
+ fixedPanels.forEach((size, panelId) => {
56
+ nextLayout[panelId] = size;
57
+ });
58
+
59
+ if (flexiblePanelsTotalPrevSize > 0) {
60
+ for (const panelId of flexiblePanelIds) {
61
+ const prevSize = prevLayout[panelId] ?? 0;
62
+ nextLayout[panelId] = formatLayoutNumber(
63
+ (prevSize / flexiblePanelsTotalPrevSize) * remainingSize,
64
+ );
65
+ }
66
+ } else {
67
+ const evenSize = formatLayoutNumber(remainingSize / flexiblePanelIds.length);
68
+ for (const panelId of flexiblePanelIds) {
69
+ nextLayout[panelId] = evenSize;
70
+ }
71
+ }
72
+
73
+ return nextLayout;
74
+ }