@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,56 @@
1
+ import type { Layout, RegisteredGroup } from '../../components/group/types';
2
+ import { getMountedGroups } from '../mutable-state/groups';
3
+ import { updateInteractionState } from '../mutable-state/interactions';
4
+ import { findMatchingHitRegions } from '../utils/findMatchingHitRegions';
5
+
6
+ export function onDocumentPointerDown(event: PointerEvent) {
7
+ if (event.defaultPrevented) {
8
+ return;
9
+ } else if (event.pointerType === 'mouse' && event.button > 0) {
10
+ return;
11
+ }
12
+
13
+ const mountedGroups = getMountedGroups();
14
+
15
+ const hitRegions = findMatchingHitRegions(event, mountedGroups);
16
+
17
+ const initialLayoutMap = new Map<RegisteredGroup, Layout>();
18
+
19
+ let didChangeFocus = false;
20
+
21
+ hitRegions.forEach((current) => {
22
+ if (current.separator) {
23
+ if (!didChangeFocus) {
24
+ didChangeFocus = true;
25
+
26
+ current.separator.element.focus({
27
+ // @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#browser_compatibility
28
+ focusVisible: false,
29
+ preventScroll: true,
30
+ });
31
+
32
+ // TRICKY
33
+ // Calling setPointerCapture() here would help with detecting pointer "pointermove"/"pointerup" events that happen over iframes
34
+ // but it would also prevent "click" events from firing if the use releases without actually dragging
35
+ // Because of this, it's safer to wait until the first "pointermove" event to set capture
36
+ }
37
+ }
38
+
39
+ const match = mountedGroups.get(current.group);
40
+ if (match) {
41
+ initialLayoutMap.set(current.group, match.layout);
42
+ }
43
+ });
44
+
45
+ updateInteractionState({
46
+ cursorFlags: 0,
47
+ hitRegions,
48
+ initialLayoutMap,
49
+ pointerDownAtPoint: { x: event.clientX, y: event.clientY },
50
+ state: 'active',
51
+ });
52
+
53
+ if (hitRegions.length) {
54
+ event.preventDefault();
55
+ }
56
+ }
@@ -0,0 +1,21 @@
1
+ import { getMountedGroups } from '../mutable-state/groups';
2
+ import { getInteractionState } from '../mutable-state/interactions';
3
+ import { updateActiveHitRegions } from '../utils/updateActiveHitRegion';
4
+
5
+ export function onDocumentPointerLeave(event: PointerEvent) {
6
+ const mountedGroups = getMountedGroups();
7
+ const interactionState = getInteractionState();
8
+
9
+ switch (interactionState.state) {
10
+ case 'active': {
11
+ updateActiveHitRegions({
12
+ document: event.currentTarget as Document,
13
+ event,
14
+ hitRegions: interactionState.hitRegions,
15
+ initialLayoutMap: interactionState.initialLayoutMap,
16
+ mountedGroups,
17
+ prevCursorFlags: interactionState.cursorFlags,
18
+ });
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,89 @@
1
+ import { updateCursorStyle } from '../cursor/updateCursorStyle';
2
+ import {
3
+ getMountedGroups,
4
+ getMountedGroupState,
5
+ updateMountedGroup,
6
+ } from '../mutable-state/groups';
7
+ import { getInteractionState, updateInteractionState } from '../mutable-state/interactions';
8
+ import { findMatchingHitRegions } from '../utils/findMatchingHitRegions';
9
+ import { updateActiveHitRegions } from '../utils/updateActiveHitRegion';
10
+
11
+ export function onDocumentPointerMove(event: PointerEvent) {
12
+ if (event.defaultPrevented) {
13
+ return;
14
+ }
15
+
16
+ const interactionState = getInteractionState();
17
+ const mountedGroups = getMountedGroups();
18
+
19
+ switch (interactionState.state) {
20
+ case 'active': {
21
+ // Edge case (see #340)
22
+ // Detect when the pointer has been released outside an iframe on a different domain
23
+ if (
24
+ // Skip this check for "pointerleave" events, else Firefox triggers a false positive (see #514)
25
+ event.buttons === 0
26
+ ) {
27
+ updateInteractionState({
28
+ cursorFlags: 0,
29
+ state: 'inactive',
30
+ });
31
+
32
+ // Dispatch one more "change" event after the interaction state has been reset.
33
+ // Groups use this as a signal to call onLayoutChanged.
34
+ // This is the missed-pointerup fallback (pointer released outside a
35
+ // cross-origin iframe, see #340) — still a real user interaction.
36
+ interactionState.hitRegions.forEach((hitRegion) => {
37
+ const groupState = getMountedGroupState(hitRegion.group.id, true);
38
+ updateMountedGroup(hitRegion.group, groupState, {
39
+ isUserInteraction: true,
40
+ });
41
+ });
42
+
43
+ return;
44
+ }
45
+
46
+ for (const hitRegion of interactionState.hitRegions) {
47
+ if (hitRegion.separator) {
48
+ const { element } = hitRegion.separator;
49
+ if (!element.hasPointerCapture?.(event.pointerId)) {
50
+ element.setPointerCapture?.(event.pointerId);
51
+ }
52
+ }
53
+ }
54
+
55
+ updateActiveHitRegions({
56
+ document: event.currentTarget as Document,
57
+ event,
58
+ hitRegions: interactionState.hitRegions,
59
+ initialLayoutMap: interactionState.initialLayoutMap,
60
+ mountedGroups,
61
+ pointerDownAtPoint: interactionState.pointerDownAtPoint,
62
+ prevCursorFlags: interactionState.cursorFlags,
63
+ });
64
+ break;
65
+ }
66
+ default: {
67
+ // Update HitRegions if a drag has not been started
68
+ const hitRegions = findMatchingHitRegions(event, mountedGroups);
69
+
70
+ if (hitRegions.length === 0) {
71
+ if (interactionState.state !== 'inactive') {
72
+ updateInteractionState({
73
+ cursorFlags: 0,
74
+ state: 'inactive',
75
+ });
76
+ }
77
+ } else {
78
+ updateInteractionState({
79
+ cursorFlags: 0,
80
+ hitRegions,
81
+ state: 'hover',
82
+ });
83
+ }
84
+
85
+ updateCursorStyle(event.currentTarget as Document);
86
+ break;
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,19 @@
1
+ import { getInteractionState, updateInteractionState } from '../mutable-state/interactions';
2
+
3
+ export function onDocumentPointerOut(event: PointerEvent) {
4
+ // For some reason, "pointerout" events don't fire if the `relatedTarget` is an iframe
5
+ // This can leave the `data-separator` attribute in an invalid state ("hover") which in turn might break styles
6
+ // The easiest fix for this case is to reset the interaction state in this specific circumstance
7
+ // See issues/645
8
+ if (event.relatedTarget instanceof HTMLIFrameElement) {
9
+ const interactionState = getInteractionState();
10
+ switch (interactionState.state) {
11
+ case 'hover': {
12
+ updateInteractionState({
13
+ cursorFlags: 0,
14
+ state: 'inactive',
15
+ });
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ import { completeActivePointerResize } from '../utils/completeActivePointerResize.ts';
2
+
3
+ export function onDocumentPointerUp(event: PointerEvent) {
4
+ if (event.defaultPrevented) {
5
+ return;
6
+ } else if (event.pointerType === 'mouse' && event.button > 0) {
7
+ return;
8
+ }
9
+
10
+ const matched = completeActivePointerResize(event.currentTarget as Document);
11
+ if (matched) {
12
+ event.preventDefault();
13
+ }
14
+ }
@@ -0,0 +1,213 @@
1
+ import type { Layout, RegisteredGroup } from '../components/group/types';
2
+ import { assert } from '../utils/assert';
3
+ import { calculateAvailableGroupSize } from './dom/calculateAvailableGroupSize';
4
+ import { calculateHitRegions } from './dom/calculateHitRegions';
5
+ import { calculatePanelConstraints } from './dom/calculatePanelConstraints';
6
+ import { onDocumentContextMenu } from './event-handlers/onDocumentContextMenu';
7
+ import { onDocumentDoubleClick } from './event-handlers/onDocumentDoubleClick';
8
+ import { onDocumentKeyDown } from './event-handlers/onDocumentKeyDown';
9
+ import { onDocumentPointerDown } from './event-handlers/onDocumentPointerDown';
10
+ import { onDocumentPointerLeave } from './event-handlers/onDocumentPointerLeave';
11
+ import { onDocumentPointerMove } from './event-handlers/onDocumentPointerMove';
12
+ import { onDocumentPointerOut } from './event-handlers/onDocumentPointerOut';
13
+ import { onDocumentPointerUp } from './event-handlers/onDocumentPointerUp';
14
+ import {
15
+ deleteMutableGroup,
16
+ getMountedGroupState,
17
+ updateMountedGroup,
18
+ } from './mutable-state/groups';
19
+ import type { SeparatorToPanelsMap } from './mutable-state/types';
20
+ import { calculateDefaultLayout } from './utils/calculateDefaultLayout';
21
+ import { layoutsEqual } from './utils/layoutsEqual';
22
+ import { notifyPanelOnResize } from './utils/notifyPanelOnResize';
23
+ import { objectsEqual } from './utils/objectsEqual';
24
+ import { preserveFixedPanelSizes } from './utils/preserveFixedPanelSizes';
25
+ import { validateLayoutKeys } from './utils/validateLayoutKeys';
26
+ import { validatePanelGroupLayout } from './utils/validatePanelGroupLayout';
27
+
28
+ const ownerDocumentReferenceCounts = new Map<Document, number>();
29
+
30
+ export function mountGroup(group: RegisteredGroup) {
31
+ let isMounted = true;
32
+
33
+ assert(group.element.ownerDocument.defaultView, 'Cannot register an unmounted Group');
34
+
35
+ const ResizeObserver = group.element.ownerDocument.defaultView.ResizeObserver;
36
+
37
+ const panelIds = new Set<string>();
38
+ const separatorIds = new Set<string>();
39
+
40
+ // Add Panels with onResize callbacks to ResizeObserver
41
+ // Add Group to ResizeObserver also in order to sync % based constraints
42
+ const resizeObserver = new ResizeObserver((entries) => {
43
+ for (const entry of entries) {
44
+ const { borderBoxSize, target } = entry;
45
+ if (target === group.element) {
46
+ if (isMounted) {
47
+ const groupSize = calculateAvailableGroupSize({ group });
48
+ if (groupSize === 0) {
49
+ // Can't calculate anything meaningful if the group has a width/height of 0
50
+ // (This could indicate that it's within a hidden subtree)
51
+ return;
52
+ }
53
+
54
+ const groupState = getMountedGroupState(group.id);
55
+ if (!groupState) {
56
+ // Not mounted yet
57
+ return;
58
+ }
59
+
60
+ // Update non-percentage based constraints
61
+ const nextDerivedPanelConstraints = calculatePanelConstraints(group);
62
+
63
+ // Revalidate layout in case constraints have changed or group size changed
64
+ const prevLayout = groupState.defaultLayoutDeferred
65
+ ? calculateDefaultLayout(nextDerivedPanelConstraints)
66
+ : groupState.layout;
67
+ const unsafeLayout = preserveFixedPanelSizes({
68
+ group,
69
+ nextGroupSize: groupSize,
70
+ prevGroupSize: groupState.groupSize,
71
+ prevLayout,
72
+ });
73
+ const nextLayout = validatePanelGroupLayout({
74
+ layout: unsafeLayout,
75
+ panelConstraints: nextDerivedPanelConstraints,
76
+ });
77
+
78
+ if (
79
+ !groupState.defaultLayoutDeferred &&
80
+ layoutsEqual(groupState.layout, nextLayout) &&
81
+ objectsEqual(groupState.derivedPanelConstraints, nextDerivedPanelConstraints) &&
82
+ groupState.groupSize === groupSize
83
+ ) {
84
+ return;
85
+ }
86
+
87
+ updateMountedGroup(group, {
88
+ defaultLayoutDeferred: false,
89
+ derivedPanelConstraints: nextDerivedPanelConstraints,
90
+ groupSize,
91
+ layout: nextLayout,
92
+ separatorToPanels: groupState.separatorToPanels,
93
+ });
94
+ }
95
+ } else {
96
+ notifyPanelOnResize(group, target as HTMLElement, borderBoxSize);
97
+ }
98
+ }
99
+ });
100
+
101
+ resizeObserver.observe(group.element);
102
+
103
+ group.panels.forEach((panel) => {
104
+ assert(
105
+ !panelIds.has(panel.id),
106
+ `Panel ids must be unique; id "${panel.id}" was used more than once`,
107
+ );
108
+
109
+ panelIds.add(panel.id);
110
+
111
+ if (panel.onResize) {
112
+ resizeObserver.observe(panel.element);
113
+ }
114
+ });
115
+
116
+ const groupSize = calculateAvailableGroupSize({ group });
117
+
118
+ // Calculate initial layout for the new Panel configuration
119
+ const derivedPanelConstraints = calculatePanelConstraints(group);
120
+ const panelIdsKey = group.panels.map(({ id }) => id).join(',');
121
+
122
+ // Gracefully handle an invalid default layout
123
+ // This could happen when e.g. useDefaultLayout is combined with dynamic Panels
124
+ // In this case the best we can do is ignore the incoming layout
125
+ let defaultLayout: Layout | undefined = group.mutableState.defaultLayout;
126
+ if (defaultLayout) {
127
+ if (!validateLayoutKeys(group.panels, defaultLayout)) {
128
+ defaultLayout = undefined;
129
+ }
130
+ }
131
+
132
+ const defaultLayoutUnsafe: Layout =
133
+ group.mutableState.layouts[panelIdsKey] ??
134
+ defaultLayout ??
135
+ calculateDefaultLayout(derivedPanelConstraints);
136
+ const defaultLayoutSafe = validatePanelGroupLayout({
137
+ layout: defaultLayoutUnsafe,
138
+ panelConstraints: derivedPanelConstraints,
139
+ });
140
+
141
+ const ownerDocument = group.element.ownerDocument;
142
+
143
+ ownerDocumentReferenceCounts.set(
144
+ ownerDocument,
145
+ (ownerDocumentReferenceCounts.get(ownerDocument) ?? 0) + 1,
146
+ );
147
+
148
+ const separatorToPanels: SeparatorToPanelsMap = new Map();
149
+ const hitRegions = calculateHitRegions(group);
150
+ hitRegions.forEach((hitRegion) => {
151
+ if (hitRegion.separator) {
152
+ separatorToPanels.set(hitRegion.separator, hitRegion.panels);
153
+ }
154
+ });
155
+
156
+ updateMountedGroup(group, {
157
+ defaultLayoutDeferred: groupSize === 0,
158
+ derivedPanelConstraints,
159
+ groupSize,
160
+ layout: defaultLayoutSafe,
161
+ separatorToPanels,
162
+ });
163
+
164
+ group.separators.forEach((separator) => {
165
+ assert(
166
+ !separatorIds.has(separator.id),
167
+ `Separator ids must be unique; id "${separator.id}" was used more than once`,
168
+ );
169
+
170
+ separatorIds.add(separator.id);
171
+
172
+ separator.element.addEventListener('keydown', onDocumentKeyDown);
173
+ });
174
+
175
+ // If this is the first group to be mounted, initialize event handlers
176
+ if (ownerDocumentReferenceCounts.get(ownerDocument) === 1) {
177
+ ownerDocument.addEventListener('contextmenu', onDocumentContextMenu, true);
178
+ ownerDocument.addEventListener('dblclick', onDocumentDoubleClick, true);
179
+ ownerDocument.addEventListener('pointerdown', onDocumentPointerDown, true);
180
+ ownerDocument.addEventListener('pointerleave', onDocumentPointerLeave);
181
+ ownerDocument.addEventListener('pointermove', onDocumentPointerMove);
182
+ ownerDocument.addEventListener('pointerout', onDocumentPointerOut);
183
+ ownerDocument.addEventListener('pointerup', onDocumentPointerUp, true);
184
+ }
185
+
186
+ return function unmountGroup() {
187
+ isMounted = false;
188
+
189
+ ownerDocumentReferenceCounts.set(
190
+ ownerDocument,
191
+ Math.max(0, (ownerDocumentReferenceCounts.get(ownerDocument) ?? 0) - 1),
192
+ );
193
+
194
+ deleteMutableGroup(group);
195
+
196
+ group.separators.forEach((separator) => {
197
+ separator.element.removeEventListener('keydown', onDocumentKeyDown);
198
+ });
199
+
200
+ // If this was the last group to be mounted, tear down event handlers
201
+ if (!ownerDocumentReferenceCounts.get(ownerDocument)) {
202
+ ownerDocument.removeEventListener('contextmenu', onDocumentContextMenu, true);
203
+ ownerDocument.removeEventListener('dblclick', onDocumentDoubleClick, true);
204
+ ownerDocument.removeEventListener('pointerdown', onDocumentPointerDown, true);
205
+ ownerDocument.removeEventListener('pointerleave', onDocumentPointerLeave);
206
+ ownerDocument.removeEventListener('pointermove', onDocumentPointerMove);
207
+ ownerDocument.removeEventListener('pointerout', onDocumentPointerOut);
208
+ ownerDocument.removeEventListener('pointerup', onDocumentPointerUp, true);
209
+ }
210
+
211
+ resizeObserver.disconnect();
212
+ };
213
+ }
@@ -0,0 +1,104 @@
1
+ import type { Layout, RegisteredGroup } from '../../components/group/types';
2
+ import type { PanelConstraints } from '../../components/panel/types';
3
+ import { EventEmitter } from '../../utils/EventEmitter';
4
+ import type { SeparatorToPanelsMap } from './types';
5
+
6
+ type State = {
7
+ defaultLayoutDeferred: boolean;
8
+ derivedPanelConstraints: PanelConstraints[];
9
+ groupSize: number;
10
+ layout: Layout;
11
+ separatorToPanels: SeparatorToPanelsMap;
12
+ };
13
+
14
+ export type MountedGroups = Map<RegisteredGroup, State>;
15
+
16
+ let map: MountedGroups = new Map();
17
+
18
+ type GroupChangeEvent = {
19
+ group: RegisteredGroup;
20
+ // True if the change was triggered by a pointer or keyboard event handler
21
+ // False for other types of resize (constraint recompute, default-size change, imperative API, etc.).
22
+ isUserInteraction: boolean;
23
+ next: State;
24
+ prev: State | undefined;
25
+ };
26
+ type GroupsChangeEvent = {
27
+ next: MountedGroups;
28
+ prev: MountedGroups;
29
+ };
30
+
31
+ const eventEmitter = new EventEmitter<{
32
+ groupChange: GroupChangeEvent;
33
+ groupsChange: GroupsChangeEvent;
34
+ }>();
35
+
36
+ export function deleteMutableGroup(group: RegisteredGroup) {
37
+ map = new Map(map);
38
+ map.delete(group);
39
+ }
40
+
41
+ export function getRegisteredGroup(groupId: string): RegisteredGroup | undefined;
42
+ export function getRegisteredGroup(groupId: string, assert: true): RegisteredGroup;
43
+ export function getRegisteredGroup(groupId: string, assert?: boolean) {
44
+ for (const [group] of map) {
45
+ if (group.id === groupId) {
46
+ return group;
47
+ }
48
+ }
49
+
50
+ if (assert) {
51
+ throw Error(`Could not find data for Group with id ${groupId}`);
52
+ }
53
+
54
+ return undefined;
55
+ }
56
+
57
+ export function getMountedGroupState(groupId: string): State | undefined;
58
+ export function getMountedGroupState(groupId: string, assert: true): State;
59
+ export function getMountedGroupState(groupId: string, assert?: boolean) {
60
+ for (const [group, mountedGroup] of map) {
61
+ if (group.id === groupId) {
62
+ return mountedGroup;
63
+ }
64
+ }
65
+
66
+ if (assert) {
67
+ throw Error(`Could not find data for Group with id ${groupId}`);
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
73
+ export function getMountedGroups() {
74
+ return map;
75
+ }
76
+
77
+ export function subscribeToMountedGroup(
78
+ groupId: string,
79
+ callback: (event: GroupChangeEvent) => void,
80
+ ) {
81
+ return eventEmitter.addListener('groupChange', (event) => {
82
+ if (event.group.id === groupId) {
83
+ callback(event);
84
+ }
85
+ });
86
+ }
87
+
88
+ export function updateMountedGroup(
89
+ group: RegisteredGroup,
90
+ next: State,
91
+ meta?: { isUserInteraction?: boolean },
92
+ ) {
93
+ const prev = map.get(group);
94
+
95
+ map = new Map(map);
96
+ map.set(group, next);
97
+
98
+ eventEmitter.emit('groupChange', {
99
+ group,
100
+ isUserInteraction: meta?.isUserInteraction === true,
101
+ prev,
102
+ next,
103
+ });
104
+ }
@@ -0,0 +1,49 @@
1
+ import { EventEmitter } from '../../utils/EventEmitter';
2
+ import type { InteractionState } from './types';
3
+
4
+ let state: InteractionState = {
5
+ cursorFlags: 0,
6
+ state: 'inactive',
7
+ };
8
+
9
+ type ChangeEvent = {
10
+ next: InteractionState;
11
+ prev: InteractionState;
12
+ };
13
+
14
+ const eventEmitter = new EventEmitter<{
15
+ change: ChangeEvent;
16
+ }>();
17
+
18
+ export function getInteractionState() {
19
+ return state;
20
+ }
21
+
22
+ export function subscribeToInteractionState(callback: (event: ChangeEvent) => void) {
23
+ return eventEmitter.addListener('change', callback);
24
+ }
25
+
26
+ export function updateCursorFlags(cursorFlags: number) {
27
+ const prev = state;
28
+
29
+ const next = { ...state };
30
+ next.cursorFlags = cursorFlags;
31
+
32
+ state = next;
33
+
34
+ eventEmitter.emit('change', {
35
+ prev,
36
+ next,
37
+ });
38
+ }
39
+
40
+ export function updateInteractionState(next: InteractionState) {
41
+ const prev = state;
42
+
43
+ state = next;
44
+
45
+ eventEmitter.emit('change', {
46
+ prev,
47
+ next,
48
+ });
49
+ }
@@ -0,0 +1,31 @@
1
+ import type { Layout, RegisteredGroup } from '../../components/group/types';
2
+ import type { RegisteredPanel } from '../../components/panel/types';
3
+ import type { RegisteredSeparator } from '../../components/separator/types';
4
+ import type { Point } from '../../types';
5
+ import type { HitRegion } from '../dom/calculateHitRegions';
6
+
7
+ export type InteractionInactive = {
8
+ cursorFlags: 0;
9
+ state: 'inactive';
10
+ };
11
+
12
+ export type InteractionHover = {
13
+ cursorFlags: 0;
14
+ hitRegions: HitRegion[];
15
+ state: 'hover';
16
+ };
17
+
18
+ export type InteractionActive = {
19
+ cursorFlags: number;
20
+ hitRegions: HitRegion[];
21
+ initialLayoutMap: Map<RegisteredGroup, Layout>;
22
+ pointerDownAtPoint: Point;
23
+ state: 'active';
24
+ };
25
+
26
+ export type InteractionState = InteractionInactive | InteractionHover | InteractionActive;
27
+
28
+ export type SeparatorToPanelsMap = Map<
29
+ RegisteredSeparator,
30
+ [primaryPanel: RegisteredPanel, secondaryPanel: RegisteredPanel]
31
+ >;
@@ -0,0 +1,6 @@
1
+ export function convertEmToPixels(element: Element, value: number) {
2
+ const style = getComputedStyle(element);
3
+ const fontSize = parseFloat(style.fontSize);
4
+
5
+ return value * fontSize;
6
+ }
@@ -0,0 +1,6 @@
1
+ export function convertRemToPixels(element: Element, value: number) {
2
+ const style = getComputedStyle(element.ownerDocument.documentElement);
3
+ const fontSize = parseFloat(style.fontSize);
4
+
5
+ return value * fontSize;
6
+ }
@@ -0,0 +1,3 @@
1
+ export function convertVhToPixels(value: number) {
2
+ return (value / 100) * window.innerHeight;
3
+ }
@@ -0,0 +1,3 @@
1
+ export function convertVwToPixels(value: number) {
2
+ return (value / 100) * window.innerWidth;
3
+ }
@@ -0,0 +1,28 @@
1
+ import type { SizeUnit } from '../../components/panel/types';
2
+
3
+ export function parseSizeAndUnit(size: number | string): [numeric: number, size: SizeUnit] {
4
+ switch (typeof size) {
5
+ case 'number': {
6
+ return [size, 'px'];
7
+ }
8
+ case 'string': {
9
+ const numeric = parseFloat(size);
10
+
11
+ if (size.endsWith('%')) {
12
+ return [numeric, '%'];
13
+ } else if (size.endsWith('px')) {
14
+ return [numeric, 'px'];
15
+ } else if (size.endsWith('rem')) {
16
+ return [numeric, 'rem'];
17
+ } else if (size.endsWith('em')) {
18
+ return [numeric, 'em'];
19
+ } else if (size.endsWith('vh')) {
20
+ return [numeric, 'vh'];
21
+ } else if (size.endsWith('vw')) {
22
+ return [numeric, 'vw'];
23
+ }
24
+
25
+ return [numeric, '%'];
26
+ }
27
+ }
28
+ }