@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.
- package/LICENSE.upstream +21 -0
- package/README.md +22 -0
- package/UPSTREAM.md +64 -0
- package/package.json +61 -0
- package/src/components/group/Group.tsrx +244 -0
- package/src/components/group/GroupContext.ts +25 -0
- package/src/components/group/auto-save/getStorageKey.ts +3 -0
- package/src/components/group/auto-save/readLegacyLayout.ts +37 -0
- package/src/components/group/sortByElementOffset.ts +20 -0
- package/src/components/group/types.ts +50 -0
- package/src/components/group/useDefaultLayout.ts +152 -0
- package/src/components/group/useGroupCallbackRef.ts +6 -0
- package/src/components/group/useGroupContext.ts +12 -0
- package/src/components/group/useGroupImperativeHandle.ts +46 -0
- package/src/components/group/useGroupRef.ts +6 -0
- package/src/components/panel/Panel.tsrx +130 -0
- package/src/components/panel/types.ts +66 -0
- package/src/components/panel/usePanelCallbackRef.ts +6 -0
- package/src/components/panel/usePanelImperativeHandle.ts +42 -0
- package/src/components/panel/usePanelRef.ts +6 -0
- package/src/components/separator/Separator.tsrx +126 -0
- package/src/components/separator/types.ts +22 -0
- package/src/constants.ts +25 -0
- package/src/global/cursor/getCursorStyle.ts +102 -0
- package/src/global/cursor/supportsAdvancedCursorStyles.ts +25 -0
- package/src/global/cursor/updateCursorStyle.ts +77 -0
- package/src/global/dom/calculateAvailableGroupSize.ts +11 -0
- package/src/global/dom/calculateHitRegions.ts +200 -0
- package/src/global/dom/calculatePanelConstraints.ts +84 -0
- package/src/global/event-handlers/onDocumentContextMenu.ts +9 -0
- package/src/global/event-handlers/onDocumentDoubleClick.ts +31 -0
- package/src/global/event-handlers/onDocumentKeyDown.ts +133 -0
- package/src/global/event-handlers/onDocumentPointerDown.ts +56 -0
- package/src/global/event-handlers/onDocumentPointerLeave.ts +21 -0
- package/src/global/event-handlers/onDocumentPointerMove.ts +89 -0
- package/src/global/event-handlers/onDocumentPointerOut.ts +19 -0
- package/src/global/event-handlers/onDocumentPointerUp.ts +14 -0
- package/src/global/mountGroup.ts +213 -0
- package/src/global/mutable-state/groups.ts +104 -0
- package/src/global/mutable-state/interactions.ts +49 -0
- package/src/global/mutable-state/types.ts +31 -0
- package/src/global/styles/convertEmToPixels.ts +6 -0
- package/src/global/styles/convertRemToPixels.ts +6 -0
- package/src/global/styles/convertVhToPixels.ts +3 -0
- package/src/global/styles/convertVwToPixels.ts +3 -0
- package/src/global/styles/parseSizeAndUnit.ts +28 -0
- package/src/global/styles/sizeStyleToPixels.ts +48 -0
- package/src/global/utils/adjustLayoutByDelta.ts +339 -0
- package/src/global/utils/adjustLayoutForSeparator.ts +54 -0
- package/src/global/utils/calculateDefaultLayout.ts +37 -0
- package/src/global/utils/calculateSeparatorAriaValues.ts +67 -0
- package/src/global/utils/compareLayoutNumbers.ts +9 -0
- package/src/global/utils/completeActivePointerResize.ts +37 -0
- package/src/global/utils/doRectsIntersect.ts +5 -0
- package/src/global/utils/findClosestHitRegion.ts +43 -0
- package/src/global/utils/findClosestRect.ts +36 -0
- package/src/global/utils/findMatchingHitRegions.ts +41 -0
- package/src/global/utils/findSeparatorGroup.ts +13 -0
- package/src/global/utils/formatLayoutNumber.ts +3 -0
- package/src/global/utils/getDistanceBetweenPointAndRect.ts +14 -0
- package/src/global/utils/getImperativeGroupMethods.ts +68 -0
- package/src/global/utils/getImperativePanelMethods.ts +228 -0
- package/src/global/utils/isCoarsePointer.ts +8 -0
- package/src/global/utils/isViableHitTarget.ts +53 -0
- package/src/global/utils/layoutNumbersEqual.ts +5 -0
- package/src/global/utils/layoutsEqual.ts +17 -0
- package/src/global/utils/notifyPanelOnResize.ts +33 -0
- package/src/global/utils/objectsEqual.ts +14 -0
- package/src/global/utils/preserveFixedPanelSizes.ts +74 -0
- package/src/global/utils/updateActiveHitRegion.ts +132 -0
- package/src/global/utils/validateLayoutKeys.ts +19 -0
- package/src/global/utils/validatePanelGroupLayout.ts +92 -0
- package/src/global/utils/validatePanelSize.ts +41 -0
- package/src/hooks/useForceUpdate.ts +8 -0
- package/src/hooks/useId.ts +6 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
- package/src/hooks/useMergedRefs.ts +18 -0
- package/src/hooks/useStableArray.ts +15 -0
- package/src/hooks/useStableCallback.ts +18 -0
- package/src/hooks/useStableObject.ts +15 -0
- package/src/index.tsrx +12 -0
- package/src/internal.ts +23 -0
- package/src/types.ts +18 -0
- package/src/utils/EventEmitter.ts +72 -0
- package/src/utils/assert.ts +8 -0
- package/src/utils/isArrayEqual.ts +12 -0
- package/src/utils/isHTMLElement.ts +9 -0
- package/src/utils/isShadowRoot.ts +9 -0
- package/src/utils/parseNumericStyleValue.ts +15 -0
- package/src/vendor/stacking-order.ts +133 -0
- package/status.json +11 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Layout, RegisteredGroup } from '../../components/group/types';
|
|
2
|
+
import {
|
|
3
|
+
CURSOR_FLAG_HORIZONTAL_MAX,
|
|
4
|
+
CURSOR_FLAG_HORIZONTAL_MIN,
|
|
5
|
+
CURSOR_FLAG_VERTICAL_MAX,
|
|
6
|
+
CURSOR_FLAG_VERTICAL_MIN,
|
|
7
|
+
CURSOR_FLAGS_HORIZONTAL,
|
|
8
|
+
CURSOR_FLAGS_VERTICAL,
|
|
9
|
+
} from '../../constants';
|
|
10
|
+
import type { Point } from '../../types';
|
|
11
|
+
import { updateCursorStyle } from '../cursor/updateCursorStyle';
|
|
12
|
+
import type { HitRegion } from '../dom/calculateHitRegions';
|
|
13
|
+
import { updateMountedGroup, type MountedGroups } from '../mutable-state/groups';
|
|
14
|
+
import { updateCursorFlags } from '../mutable-state/interactions';
|
|
15
|
+
import { adjustLayoutByDelta } from './adjustLayoutByDelta';
|
|
16
|
+
import { layoutsEqual } from './layoutsEqual';
|
|
17
|
+
|
|
18
|
+
export function updateActiveHitRegions({
|
|
19
|
+
document,
|
|
20
|
+
event,
|
|
21
|
+
hitRegions,
|
|
22
|
+
initialLayoutMap,
|
|
23
|
+
mountedGroups,
|
|
24
|
+
pointerDownAtPoint,
|
|
25
|
+
prevCursorFlags,
|
|
26
|
+
}: {
|
|
27
|
+
document: Document;
|
|
28
|
+
event: {
|
|
29
|
+
clientX: number;
|
|
30
|
+
clientY: number;
|
|
31
|
+
movementX: number;
|
|
32
|
+
movementY: number;
|
|
33
|
+
};
|
|
34
|
+
hitRegions: HitRegion[];
|
|
35
|
+
initialLayoutMap: Map<RegisteredGroup, Layout>;
|
|
36
|
+
mountedGroups: MountedGroups;
|
|
37
|
+
pointerDownAtPoint?: Point;
|
|
38
|
+
prevCursorFlags: number;
|
|
39
|
+
}) {
|
|
40
|
+
let nextCursorFlags = 0;
|
|
41
|
+
|
|
42
|
+
// Note that HitRegions are frozen once a drag has started
|
|
43
|
+
// Modify the Group layouts for all matching HitRegions though
|
|
44
|
+
hitRegions.forEach((current) => {
|
|
45
|
+
const { group, groupSize } = current;
|
|
46
|
+
const { orientation, panels } = group;
|
|
47
|
+
const { disableCursor } = group.mutableState;
|
|
48
|
+
|
|
49
|
+
let deltaAsPercentage = 0;
|
|
50
|
+
if (pointerDownAtPoint) {
|
|
51
|
+
if (orientation === 'horizontal') {
|
|
52
|
+
deltaAsPercentage = ((event.clientX - pointerDownAtPoint.x) / groupSize) * 100;
|
|
53
|
+
} else {
|
|
54
|
+
deltaAsPercentage = ((event.clientY - pointerDownAtPoint.y) / groupSize) * 100;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (orientation === 'horizontal') {
|
|
58
|
+
deltaAsPercentage = event.clientX < 0 ? -100 : 100;
|
|
59
|
+
} else {
|
|
60
|
+
deltaAsPercentage = event.clientY < 0 ? -100 : 100;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const initialLayout = initialLayoutMap.get(group);
|
|
65
|
+
const groupState = mountedGroups.get(group);
|
|
66
|
+
if (!initialLayout || !groupState) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
defaultLayoutDeferred,
|
|
72
|
+
derivedPanelConstraints,
|
|
73
|
+
groupSize: mountedGroupSize,
|
|
74
|
+
layout: prevLayout,
|
|
75
|
+
separatorToPanels,
|
|
76
|
+
} = groupState;
|
|
77
|
+
if (derivedPanelConstraints && prevLayout && separatorToPanels) {
|
|
78
|
+
const nextLayout = adjustLayoutByDelta({
|
|
79
|
+
delta: deltaAsPercentage,
|
|
80
|
+
initialLayout,
|
|
81
|
+
panelConstraints: derivedPanelConstraints,
|
|
82
|
+
pivotIndices: current.panels.map((panel) => panels.indexOf(panel)),
|
|
83
|
+
prevLayout,
|
|
84
|
+
trigger: 'mouse-or-touch',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (layoutsEqual(nextLayout, prevLayout)) {
|
|
88
|
+
if (deltaAsPercentage !== 0 && !disableCursor) {
|
|
89
|
+
// An unchanged means the cursor has exceeded the allowed bounds
|
|
90
|
+
switch (orientation) {
|
|
91
|
+
case 'horizontal': {
|
|
92
|
+
nextCursorFlags |=
|
|
93
|
+
deltaAsPercentage < 0 ? CURSOR_FLAG_HORIZONTAL_MIN : CURSOR_FLAG_HORIZONTAL_MAX;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'vertical': {
|
|
97
|
+
nextCursorFlags |=
|
|
98
|
+
deltaAsPercentage < 0 ? CURSOR_FLAG_VERTICAL_MIN : CURSOR_FLAG_VERTICAL_MAX;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
updateMountedGroup(current.group, {
|
|
105
|
+
defaultLayoutDeferred,
|
|
106
|
+
derivedPanelConstraints: derivedPanelConstraints,
|
|
107
|
+
groupSize: mountedGroupSize,
|
|
108
|
+
layout: nextLayout,
|
|
109
|
+
separatorToPanels,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Edge case
|
|
116
|
+
// Re-use previous horizontal/vertical cursor flags if there's been no movement since the last event
|
|
117
|
+
// This accounts for edge cases in browsers like Firefox that sometimes round clientX/clientY values
|
|
118
|
+
let cursorFlags = 0;
|
|
119
|
+
if (event.movementX === 0) {
|
|
120
|
+
cursorFlags |= prevCursorFlags & CURSOR_FLAGS_HORIZONTAL;
|
|
121
|
+
} else {
|
|
122
|
+
cursorFlags |= nextCursorFlags & CURSOR_FLAGS_HORIZONTAL;
|
|
123
|
+
}
|
|
124
|
+
if (event.movementY === 0) {
|
|
125
|
+
cursorFlags |= prevCursorFlags & CURSOR_FLAGS_VERTICAL;
|
|
126
|
+
} else {
|
|
127
|
+
cursorFlags |= nextCursorFlags & CURSOR_FLAGS_VERTICAL;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
updateCursorFlags(cursorFlags);
|
|
131
|
+
updateCursorStyle(document);
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Layout } from '../../components/group/types';
|
|
2
|
+
import type { RegisteredPanel } from '../../components/panel/types';
|
|
3
|
+
|
|
4
|
+
export function validateLayoutKeys(panels: RegisteredPanel[], layout: Layout) {
|
|
5
|
+
const panelIds = panels.map((panel) => panel.id);
|
|
6
|
+
const layoutKeys = Object.keys(layout);
|
|
7
|
+
|
|
8
|
+
if (panelIds.length !== layoutKeys.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const panelId of panelIds) {
|
|
13
|
+
if (!layoutKeys.includes(panelId)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Layout } from '../../components/group/types';
|
|
2
|
+
import type { PanelConstraints } from '../../components/panel/types';
|
|
3
|
+
import { assert } from '../../utils/assert';
|
|
4
|
+
import { layoutNumbersEqual } from './layoutNumbersEqual';
|
|
5
|
+
import { validatePanelSize } from './validatePanelSize';
|
|
6
|
+
|
|
7
|
+
// All units must be in percentages; pixel values should be pre-converted
|
|
8
|
+
export function validatePanelGroupLayout({
|
|
9
|
+
layout,
|
|
10
|
+
panelConstraints,
|
|
11
|
+
}: {
|
|
12
|
+
layout: Layout;
|
|
13
|
+
panelConstraints: PanelConstraints[];
|
|
14
|
+
}): Layout {
|
|
15
|
+
const prevLayout = Object.values(layout);
|
|
16
|
+
const nextLayout = [...prevLayout];
|
|
17
|
+
|
|
18
|
+
const nextLayoutTotalSize = nextLayout.reduce((accumulated, current) => accumulated + current, 0);
|
|
19
|
+
|
|
20
|
+
// Validate layout expectations
|
|
21
|
+
if (nextLayout.length !== panelConstraints.length) {
|
|
22
|
+
throw Error(
|
|
23
|
+
`Invalid ${panelConstraints.length} panel layout: ${nextLayout
|
|
24
|
+
.map((size) => `${size}%`)
|
|
25
|
+
.join(', ')}`,
|
|
26
|
+
);
|
|
27
|
+
} else if (!layoutNumbersEqual(nextLayoutTotalSize, 100) && nextLayout.length > 0) {
|
|
28
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
29
|
+
const unsafeSize = nextLayout[index];
|
|
30
|
+
assert(unsafeSize != null, `No layout data found for index ${index}`);
|
|
31
|
+
const safeSize = (100 / nextLayoutTotalSize) * unsafeSize;
|
|
32
|
+
nextLayout[index] = safeSize;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let remainingSize = 0;
|
|
37
|
+
|
|
38
|
+
// First pass: Validate the proposed layout given each panel's constraints
|
|
39
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
40
|
+
const prevSize = prevLayout[index];
|
|
41
|
+
assert(prevSize != null, `No layout data found for index ${index}`);
|
|
42
|
+
|
|
43
|
+
const unsafeSize = nextLayout[index];
|
|
44
|
+
assert(unsafeSize != null, `No layout data found for index ${index}`);
|
|
45
|
+
|
|
46
|
+
const safeSize = validatePanelSize({
|
|
47
|
+
overrideDisabledPanels: true,
|
|
48
|
+
panelConstraints: panelConstraints[index],
|
|
49
|
+
prevSize,
|
|
50
|
+
size: unsafeSize,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (unsafeSize != safeSize) {
|
|
54
|
+
remainingSize += unsafeSize - safeSize;
|
|
55
|
+
|
|
56
|
+
nextLayout[index] = safeSize;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If there is additional, left over space, assign it to any panel(s) that permits it
|
|
61
|
+
// (It's not worth taking multiple additional passes to evenly distribute)
|
|
62
|
+
if (!layoutNumbersEqual(remainingSize, 0)) {
|
|
63
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
64
|
+
const prevSize = nextLayout[index];
|
|
65
|
+
assert(prevSize != null, `No layout data found for index ${index}`);
|
|
66
|
+
const unsafeSize = prevSize + remainingSize;
|
|
67
|
+
const safeSize = validatePanelSize({
|
|
68
|
+
overrideDisabledPanels: true,
|
|
69
|
+
panelConstraints: panelConstraints[index],
|
|
70
|
+
prevSize,
|
|
71
|
+
size: unsafeSize,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (prevSize !== safeSize) {
|
|
75
|
+
remainingSize -= safeSize - prevSize;
|
|
76
|
+
nextLayout[index] = safeSize;
|
|
77
|
+
|
|
78
|
+
// Once we've used up the remainder, bail
|
|
79
|
+
if (layoutNumbersEqual(remainingSize, 0)) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const prevLayoutKeys = Object.keys(layout);
|
|
87
|
+
|
|
88
|
+
return nextLayout.reduce<Layout>((accumulated, current, index) => {
|
|
89
|
+
accumulated[prevLayoutKeys[index]] = current;
|
|
90
|
+
return accumulated;
|
|
91
|
+
}, {});
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PanelConstraints } from '../../components/panel/types';
|
|
2
|
+
import { compareLayoutNumbers } from './compareLayoutNumbers';
|
|
3
|
+
import { formatLayoutNumber } from './formatLayoutNumber';
|
|
4
|
+
|
|
5
|
+
// Panel size must be in percentages; pixel values should be pre-converted
|
|
6
|
+
export function validatePanelSize({
|
|
7
|
+
overrideDisabledPanels,
|
|
8
|
+
panelConstraints,
|
|
9
|
+
prevSize,
|
|
10
|
+
size,
|
|
11
|
+
}: {
|
|
12
|
+
overrideDisabledPanels?: boolean;
|
|
13
|
+
panelConstraints: PanelConstraints;
|
|
14
|
+
prevSize: number;
|
|
15
|
+
size: number;
|
|
16
|
+
}) {
|
|
17
|
+
const { collapsedSize = 0, collapsible, disabled, maxSize = 100, minSize = 0 } = panelConstraints;
|
|
18
|
+
|
|
19
|
+
if (disabled && !overrideDisabledPanels) {
|
|
20
|
+
return prevSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (compareLayoutNumbers(size, minSize) < 0) {
|
|
24
|
+
if (collapsible) {
|
|
25
|
+
// Collapsible panels should snap closed or open only once they cross the halfway point between collapsed and min size.
|
|
26
|
+
const halfwayPoint = (collapsedSize + minSize) / 2;
|
|
27
|
+
if (compareLayoutNumbers(size, halfwayPoint) < 0) {
|
|
28
|
+
size = collapsedSize;
|
|
29
|
+
} else {
|
|
30
|
+
size = minSize;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
size = minSize;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
size = Math.min(maxSize, size);
|
|
38
|
+
size = formatLayoutNumber(size);
|
|
39
|
+
|
|
40
|
+
return size;
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useCallback, useState } from 'octane';
|
|
2
|
+
import { subSlot } from '../internal';
|
|
3
|
+
|
|
4
|
+
export function useForceUpdate(slot?: symbol) {
|
|
5
|
+
const [sigil, setSigil] = useState({}, subSlot(slot, 'state'));
|
|
6
|
+
const forceUpdate = useCallback(() => setSigil({}), [], subSlot(slot, 'callback'));
|
|
7
|
+
return [sigil as unknown, forceUpdate] as const;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
2
|
+
import { useStableCallback } from './useStableCallback';
|
|
3
|
+
|
|
4
|
+
type PossibleRef<Type> = Octane.Ref<Type> | undefined;
|
|
5
|
+
|
|
6
|
+
export function useMergedRefs<Type>(...args: Array<PossibleRef<Type> | symbol>) {
|
|
7
|
+
const maybeSlot = args.at(-1);
|
|
8
|
+
const slot = typeof maybeSlot === 'symbol' ? maybeSlot : undefined;
|
|
9
|
+
const refs = (slot === undefined ? args : args.slice(0, -1)) as PossibleRef<Type>[];
|
|
10
|
+
return useStableCallback((value: Type | null) => {
|
|
11
|
+
const assign = (ref: PossibleRef<Type>) => {
|
|
12
|
+
if (Array.isArray(ref)) for (const child of ref) assign(child);
|
|
13
|
+
else if (typeof ref === 'function') ref(value);
|
|
14
|
+
else if (ref) (ref as { current: Type | null }).current = value;
|
|
15
|
+
};
|
|
16
|
+
for (const ref of refs) assign(ref);
|
|
17
|
+
}, slot);
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRef } from 'octane';
|
|
2
|
+
import { subSlot } from '../internal';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
4
|
+
|
|
5
|
+
export function useStableArray<Type>(value: Type[], slot?: symbol): Type[] {
|
|
6
|
+
const ref = useRef<Type[]>([...value], subSlot(slot, 'ref'));
|
|
7
|
+
useIsomorphicLayoutEffect(
|
|
8
|
+
() => {
|
|
9
|
+
ref.current.splice(0, ref.current.length, ...value);
|
|
10
|
+
},
|
|
11
|
+
[value],
|
|
12
|
+
subSlot(slot, 'effect'),
|
|
13
|
+
);
|
|
14
|
+
return ref.current;
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'octane';
|
|
2
|
+
import { subSlot } from '../internal';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
4
|
+
|
|
5
|
+
export function useStableCallback<Args extends readonly unknown[], Result>(
|
|
6
|
+
fn: (...args: Args) => Result,
|
|
7
|
+
slot?: symbol,
|
|
8
|
+
): (...args: Args) => Result {
|
|
9
|
+
const ref = useRef(fn, subSlot(slot, 'ref'));
|
|
10
|
+
useIsomorphicLayoutEffect(
|
|
11
|
+
() => {
|
|
12
|
+
ref.current = fn;
|
|
13
|
+
},
|
|
14
|
+
[fn],
|
|
15
|
+
subSlot(slot, 'effect'),
|
|
16
|
+
);
|
|
17
|
+
return useCallback((...args: Args) => ref.current(...args), [], subSlot(slot, 'callback'));
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRef } from 'octane';
|
|
2
|
+
import { subSlot } from '../internal';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
4
|
+
|
|
5
|
+
export function useStableObject<Type extends object>(value: Type, slot?: symbol): Type {
|
|
6
|
+
const ref = useRef<Type>({ ...value }, subSlot(slot, 'ref'));
|
|
7
|
+
useIsomorphicLayoutEffect(
|
|
8
|
+
() => {
|
|
9
|
+
Object.assign(ref.current, value);
|
|
10
|
+
},
|
|
11
|
+
[value],
|
|
12
|
+
subSlot(slot, 'effect'),
|
|
13
|
+
);
|
|
14
|
+
return ref.current;
|
|
15
|
+
}
|
package/src/index.tsrx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Group } from './components/group/Group.tsrx';
|
|
2
|
+
export { Panel } from './components/panel/Panel.tsrx';
|
|
3
|
+
export { Separator } from './components/separator/Separator.tsrx';
|
|
4
|
+
export { useGroupCallbackRef } from './components/group/useGroupCallbackRef';
|
|
5
|
+
export { useGroupRef } from './components/group/useGroupRef';
|
|
6
|
+
export { useDefaultLayout } from './components/group/useDefaultLayout';
|
|
7
|
+
export { usePanelCallbackRef } from './components/panel/usePanelCallbackRef';
|
|
8
|
+
export { usePanelRef } from './components/panel/usePanelRef';
|
|
9
|
+
export { isCoarsePointer } from './global/utils/isCoarsePointer';
|
|
10
|
+
export type { GroupImperativeHandle, GroupProps, Layout, LayoutChangedMeta, LayoutStorage, OnGroupLayoutChange, Orientation } from './components/group/types';
|
|
11
|
+
export type { OnPanelResize, PanelImperativeHandle, PanelProps, PanelSize, SizeUnit } from './components/panel/types';
|
|
12
|
+
export type { SeparatorProps } from './components/separator/types';
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const subSlotCache = new Map<symbol, Map<string, symbol>>();
|
|
2
|
+
const bareTagCache = new Map<string, symbol>();
|
|
3
|
+
|
|
4
|
+
export function subSlot(slot: symbol | undefined, tag: string): symbol {
|
|
5
|
+
if (slot === undefined) {
|
|
6
|
+
let bare = bareTagCache.get(tag);
|
|
7
|
+
if (bare === undefined)
|
|
8
|
+
bareTagCache.set(tag, (bare = Symbol.for(`react-resizable-panels:${tag}`)));
|
|
9
|
+
return bare;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let tags = subSlotCache.get(slot);
|
|
13
|
+
if (tags === undefined) subSlotCache.set(slot, (tags = new Map()));
|
|
14
|
+
let child = tags.get(tag);
|
|
15
|
+
if (child === undefined) tags.set(tag, (child = Symbol.for(`${slot.description ?? ''}:${tag}`)));
|
|
16
|
+
return child;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function splitSlot(args: unknown[]): [unknown[], symbol | undefined] {
|
|
20
|
+
const tail = args[args.length - 1];
|
|
21
|
+
const slot = typeof tail === 'symbol' ? tail : undefined;
|
|
22
|
+
return [slot === undefined ? args : args.slice(0, -1), slot];
|
|
23
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Dimensions = { height: number; width: number };
|
|
2
|
+
export type Point = { x: number; y: number };
|
|
3
|
+
export type PointerPrecision = { coarse: number; precise: number };
|
|
4
|
+
export type Rect = Dimensions & Point;
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
Layout,
|
|
8
|
+
LayoutChangedMeta,
|
|
9
|
+
LayoutStorage,
|
|
10
|
+
Orientation,
|
|
11
|
+
} from './components/group/types';
|
|
12
|
+
export type {
|
|
13
|
+
GroupResizeBehavior,
|
|
14
|
+
PanelConstraints,
|
|
15
|
+
PanelImperativeHandle,
|
|
16
|
+
PanelSize,
|
|
17
|
+
SizeUnit,
|
|
18
|
+
} from './components/panel/types';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type EventMap = {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type EventListener<Data> = (data: Data) => void;
|
|
6
|
+
|
|
7
|
+
export class EventEmitter<Events extends EventMap> {
|
|
8
|
+
#listenerMap: {
|
|
9
|
+
[Key in keyof Events]?: EventListener<Events[Key]>[];
|
|
10
|
+
} = {};
|
|
11
|
+
|
|
12
|
+
addListener<Type extends keyof Events>(type: Type, listener: EventListener<Events[Type]>) {
|
|
13
|
+
const listeners = this.#listenerMap[type];
|
|
14
|
+
if (listeners === undefined) {
|
|
15
|
+
this.#listenerMap[type] = [listener];
|
|
16
|
+
} else {
|
|
17
|
+
if (!listeners.includes(listener)) {
|
|
18
|
+
listeners.push(listener);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
this.removeListener(type, listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
emit<Type extends keyof Events>(type: Type, data: Events[Type]) {
|
|
28
|
+
const listeners = this.#listenerMap[type];
|
|
29
|
+
if (listeners !== undefined) {
|
|
30
|
+
if (listeners.length === 1) {
|
|
31
|
+
const listener = listeners[0];
|
|
32
|
+
listener.call(null, data);
|
|
33
|
+
} else {
|
|
34
|
+
let didThrow = false;
|
|
35
|
+
let caughtError = null;
|
|
36
|
+
|
|
37
|
+
// Clone the current listeners before calling
|
|
38
|
+
// in case calling triggers listeners to be added or removed
|
|
39
|
+
const clonedListeners = Array.from(listeners);
|
|
40
|
+
for (let i = 0; i < clonedListeners.length; i++) {
|
|
41
|
+
const listener = clonedListeners[i];
|
|
42
|
+
try {
|
|
43
|
+
listener.call(null, data);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (caughtError === null) {
|
|
46
|
+
didThrow = true;
|
|
47
|
+
caughtError = error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (didThrow) {
|
|
53
|
+
throw caughtError;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
removeAllListeners() {
|
|
60
|
+
this.#listenerMap = {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeListener<Type extends keyof Events>(type: Type, listener: EventListener<Events[Type]>) {
|
|
64
|
+
const listeners = this.#listenerMap[type];
|
|
65
|
+
if (listeners !== undefined) {
|
|
66
|
+
const index = listeners.indexOf(listener);
|
|
67
|
+
if (index >= 0) {
|
|
68
|
+
listeners.splice(index, 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Detects HTMLElement without requiring instanceof and browser globals
|
|
2
|
+
export function isHTMLElement(value: unknown): value is HTMLElement {
|
|
3
|
+
return (
|
|
4
|
+
value !== null &&
|
|
5
|
+
typeof value === 'object' &&
|
|
6
|
+
'nodeType' in value &&
|
|
7
|
+
value.nodeType === Node.ELEMENT_NODE
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Detects ShadowRoot without requiring instanceof and browser globals
|
|
2
|
+
export function isShadowRoot(value: unknown): value is ShadowRoot {
|
|
3
|
+
return (
|
|
4
|
+
value !== null &&
|
|
5
|
+
typeof value === 'object' &&
|
|
6
|
+
'nodeType' in value &&
|
|
7
|
+
value.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function parseNumericStyleValue(value: number | string | undefined): number | undefined {
|
|
2
|
+
if (value !== undefined) {
|
|
3
|
+
switch (typeof value) {
|
|
4
|
+
case 'number': {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
case 'string': {
|
|
8
|
+
if (value.endsWith('px')) {
|
|
9
|
+
return parseFloat(value);
|
|
10
|
+
}
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|