@window-splitter/vue 0.8.5

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/src/Panel.vue ADDED
@@ -0,0 +1,195 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ getPanelDomAttributes,
4
+ PanelHandle,
5
+ SharedPanelProps,
6
+ } from "@window-splitter/interface";
7
+ import {
8
+ getPanelPercentageSize,
9
+ getPanelPixelSize,
10
+ GroupMachineContextValue,
11
+ haveConstraintsChangedForPanel,
12
+ initializePanel,
13
+ isPanelData,
14
+ PanelData,
15
+ SendFn,
16
+ } from "@window-splitter/state";
17
+ import {
18
+ computed,
19
+ inject,
20
+ onMounted,
21
+ onUnmounted,
22
+ Ref,
23
+ useId,
24
+ watchEffect,
25
+ } from "vue";
26
+
27
+ // eslint-disable-next-line vue/require-default-prop
28
+ type PanelProps = SharedPanelProps<boolean> & { id?: string };
29
+
30
+ const props = withDefaults(defineProps<PanelProps>(), { collapsed: undefined });
31
+ const {
32
+ min: _min,
33
+ max: _max,
34
+ id,
35
+ collapsible: _collapsible,
36
+ collapsedSize: _collapsedSize,
37
+ onCollapseChange: _onCollapseChange,
38
+ collapseAnimation: _collapseAnimation,
39
+ onResize: _onResize,
40
+ defaultCollapsed,
41
+ default: defaultSize,
42
+ isStaticAtRest,
43
+ collapsed: _collapsed,
44
+ ...attrs
45
+ } = props;
46
+
47
+ const panelId = id || useId();
48
+ const send = inject<SendFn>("send");
49
+ const state = inject<Ref<GroupMachineContextValue>>("state");
50
+ const isPrerender = inject<Ref<boolean>>("isPrerender");
51
+
52
+ const initPanel = (): PanelData =>
53
+ initializePanel({
54
+ id: panelId,
55
+ min: props.min,
56
+ max: props.max,
57
+ collapsible: props.collapsible,
58
+ collapsed: props.collapsed,
59
+ collapsedSize: props.collapsedSize,
60
+ onCollapseChange: props.onCollapseChange
61
+ ? { current: props.onCollapseChange }
62
+ : undefined,
63
+ collapseAnimation: props.collapseAnimation,
64
+ onResize: props.onResize ? { current: props.onResize } : undefined,
65
+ defaultCollapsed,
66
+ default: defaultSize,
67
+ isStaticAtRest,
68
+ });
69
+
70
+ const panelData = computed(() => {
71
+ const item = state?.value?.items.find((i) => i.id === panelId);
72
+ if (!item || !isPanelData(item)) return undefined;
73
+ return item;
74
+ });
75
+
76
+ let dynamicPanelIsMounting = false;
77
+
78
+ if (panelData.value) {
79
+ send?.({
80
+ type: "rebindPanelCallbacks",
81
+ data: {
82
+ id: panelId,
83
+ onCollapseChange: props.onCollapseChange
84
+ ? { current: props.onCollapseChange }
85
+ : undefined,
86
+ onResize: props.onResize ? { current: props.onResize } : undefined,
87
+ },
88
+ });
89
+ } else {
90
+ if (isPrerender?.value) {
91
+ send?.({ type: "registerPanel", data: initPanel() });
92
+ } else {
93
+ dynamicPanelIsMounting = true;
94
+ }
95
+ }
96
+
97
+ onMounted(() => {
98
+ const groupId = state?.value?.groupId;
99
+ if (!groupId || !dynamicPanelIsMounting) return;
100
+
101
+ const groupElement = document.getElementById(groupId);
102
+ if (!groupElement) return;
103
+
104
+ const panelElement = document.getElementById(panelId);
105
+ if (!panelElement) return;
106
+
107
+ const order = Array.from(groupElement.children).indexOf(panelElement);
108
+ if (typeof order !== "number") return;
109
+
110
+ send?.({
111
+ type: "registerDynamicPanel",
112
+ data: { ...initPanel(), order },
113
+ });
114
+ dynamicPanelIsMounting = false;
115
+ });
116
+
117
+ const contraintChanged = computed(
118
+ () =>
119
+ !dynamicPanelIsMounting &&
120
+ haveConstraintsChangedForPanel(initPanel(), panelData.value),
121
+ );
122
+
123
+ watchEffect(() => {
124
+ if (!contraintChanged.value) return;
125
+ send?.({ type: "updateConstraints", data: initPanel() });
126
+ });
127
+
128
+ onUnmounted(() => {
129
+ requestAnimationFrame(() => send?.({ type: "unregisterPanel", id: panelId }));
130
+ });
131
+
132
+ defineExpose<PanelHandle>({
133
+ getId: () => panelId,
134
+ collapse: () => {
135
+ if (!panelData.value?.collapsible) return;
136
+ send?.({ type: "collapsePanel", panelId: panelId, controlled: true });
137
+ },
138
+ isCollapsed: () =>
139
+ Boolean(panelData.value?.collapsible && panelData.value?.collapsed),
140
+ expand: () => {
141
+ if (!panelData.value?.collapsible) return;
142
+ send?.({ type: "expandPanel", panelId: panelId, controlled: true });
143
+ },
144
+ isExpanded: () =>
145
+ Boolean(panelData.value?.collapsible && !panelData.value?.collapsed),
146
+ getPixelSize: () => getPanelPixelSize(state!.value, panelId),
147
+ getPercentageSize: () => getPanelPercentageSize(state!.value, panelId),
148
+ setSize: (size) =>
149
+ send?.({ type: "setPanelPixelSize", panelId: panelId, size }),
150
+ });
151
+
152
+ const isControlledCollapse = computed(
153
+ () => panelData.value?.collapseIsControlled,
154
+ );
155
+
156
+ watchEffect(() => {
157
+ // Subscribe to the collapsed prop (doesn't work in rAF)
158
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
159
+ props.collapsed;
160
+
161
+ requestAnimationFrame(() => {
162
+ if (!isControlledCollapse.value) return;
163
+ if (props.collapsed) {
164
+ send?.({ type: "collapsePanel", panelId: panelId, controlled: true });
165
+ } else {
166
+ send?.({ type: "expandPanel", panelId: panelId, controlled: true });
167
+ }
168
+ });
169
+ });
170
+
171
+ const computedProps = computed(() => {
172
+ const currentPanel = panelData.value;
173
+
174
+ return {
175
+ ...attrs,
176
+ ...getPanelDomAttributes({
177
+ groupId: state?.value?.groupId,
178
+ id: panelId,
179
+ collapsible: currentPanel?.collapsible,
180
+ collapsed: currentPanel?.collapsed,
181
+ }),
182
+ style: {
183
+ minWidth: 0,
184
+ minHeight: 0,
185
+ overflow: "hidden",
186
+ },
187
+ };
188
+ });
189
+ </script>
190
+
191
+ <template>
192
+ <div v-bind="computedProps">
193
+ <slot />
194
+ </div>
195
+ </template>
@@ -0,0 +1,166 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ measureGroupChildren,
4
+ PanelGroupHandle,
5
+ SharedPanelGroupProps,
6
+ } from "@window-splitter/interface";
7
+ import {
8
+ buildTemplate,
9
+ getPanelGroupPercentageSizes,
10
+ getPanelGroupPixelSizes,
11
+ groupMachine,
12
+ GroupMachineContextValue,
13
+ isPanelData,
14
+ prepareItems,
15
+ prepareSnapshot,
16
+ } from "@window-splitter/state";
17
+ import {
18
+ useId,
19
+ provide,
20
+ ref,
21
+ computed,
22
+ onMounted,
23
+ watchEffect,
24
+ onWatcherCleanup,
25
+ } from "vue";
26
+
27
+ // eslint-disable-next-line vue/require-default-prop
28
+ type PanelGroupProps = SharedPanelGroupProps & { id?: string };
29
+
30
+ const {
31
+ orientation = "horizontal",
32
+ autosaveStrategy = "localStorage",
33
+ snapshot: snapshotProp,
34
+ autosaveId,
35
+ id,
36
+ ...attrs
37
+ } = defineProps<PanelGroupProps>();
38
+
39
+ let snapshot = snapshotProp;
40
+
41
+ if (
42
+ !snapshot &&
43
+ typeof window !== "undefined" &&
44
+ autosaveId &&
45
+ autosaveStrategy === "localStorage"
46
+ ) {
47
+ const localSnapshot = localStorage.getItem(autosaveId);
48
+
49
+ if (localSnapshot) {
50
+ snapshot = JSON.parse(localSnapshot) as GroupMachineContextValue;
51
+ }
52
+ }
53
+
54
+ const isPrerender = ref(true);
55
+ provide("isPrerender", isPrerender);
56
+
57
+ const groupId = autosaveId || id || useId();
58
+ const [initialState, send, machineState] = groupMachine(
59
+ {
60
+ orientation: orientation,
61
+ groupId,
62
+ autosaveStrategy: autosaveStrategy,
63
+ ...(snapshot ? prepareSnapshot(snapshot) : undefined),
64
+ },
65
+ (s) => {
66
+ context.value = { ...s };
67
+ },
68
+ );
69
+
70
+ onMounted(() => {
71
+ isPrerender.value = false;
72
+ });
73
+
74
+ const context = ref(initialState);
75
+ const gridStyle = computed(() => {
76
+ const template = buildTemplate(context.value);
77
+ return {
78
+ height: "100%",
79
+ display: "grid",
80
+ gridTemplateColumns: orientation === "horizontal" ? template : undefined,
81
+ gridTemplateRows: orientation === "vertical" ? template : undefined,
82
+ };
83
+ });
84
+
85
+ provide("send", send);
86
+ provide("state", context);
87
+ provide("id", machineState);
88
+
89
+ const elementRef = ref<HTMLDivElement | null>(null);
90
+
91
+ onMounted(() => {
92
+ const observer = new ResizeObserver(([entry]) => {
93
+ if (!entry) return;
94
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
95
+ send?.({ type: "setSize", size: entry.contentRect });
96
+ }
97
+ });
98
+
99
+ if (elementRef.value) {
100
+ observer.observe(elementRef.value);
101
+ }
102
+
103
+ return () => observer.disconnect();
104
+ });
105
+
106
+ const childIds = computed(() =>
107
+ context.value?.items.map((i) => i.id).join(","),
108
+ );
109
+
110
+ onMounted(() => {
111
+ watchEffect(() => {
112
+ // re-render when the childIds change
113
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
114
+ childIds.value;
115
+
116
+ const cleanup = measureGroupChildren(groupId, (childrenSizes) => {
117
+ send({ type: "setActualItemsSize", childrenSizes });
118
+ });
119
+
120
+ onWatcherCleanup(cleanup);
121
+ });
122
+ });
123
+
124
+ onMounted(() => {
125
+ send({ type: "unlockGroup" });
126
+ return () => send({ type: "lockGroup" });
127
+ });
128
+
129
+ defineExpose<PanelGroupHandle>({
130
+ getId: () => groupId,
131
+ getPixelSizes: () => getPanelGroupPixelSizes(context.value),
132
+ getPercentageSizes: () => getPanelGroupPercentageSizes(context.value),
133
+ getTemplate: () =>
134
+ buildTemplate({
135
+ ...context.value,
136
+ items: prepareItems(context.value),
137
+ }),
138
+ getState: () => (machineState.current === "idle" ? "idle" : "dragging"),
139
+ setSizes: (updates) => {
140
+ for (let index = 0; index < updates.length; index++) {
141
+ const item = context.value?.items[index];
142
+ const update = updates[index];
143
+
144
+ if (item && isPanelData(item) && update) {
145
+ send({
146
+ type: "setPanelPixelSize",
147
+ panelId: item.id,
148
+ size: update,
149
+ });
150
+ }
151
+ }
152
+ },
153
+ });
154
+ </script>
155
+
156
+ <template>
157
+ <div
158
+ v-bind="attrs"
159
+ :id="groupId"
160
+ ref="elementRef"
161
+ data-panel-group-wrapper
162
+ :style="gridStyle"
163
+ >
164
+ <slot />
165
+ </div>
166
+ </template>
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ getPanelResizerDomAttributes,
4
+ move,
5
+ SharedPanelResizerProps,
6
+ } from "@window-splitter/interface";
7
+ import {
8
+ getCollapsiblePanelForHandleId,
9
+ getCursor,
10
+ getGroupSize,
11
+ GroupMachineContextValue,
12
+ haveConstraintsChangedForPanelHandle,
13
+ initializePanelHandleData,
14
+ isPanelData,
15
+ isPanelHandle,
16
+ PanelHandleData,
17
+ SendFn,
18
+ } from "@window-splitter/state";
19
+ import {
20
+ computed,
21
+ inject,
22
+ onMounted,
23
+ onUnmounted,
24
+ Ref,
25
+ useId,
26
+ watchEffect,
27
+ } from "vue";
28
+
29
+ // eslint-disable-next-line vue/require-default-prop
30
+ type PanelResizerProps = SharedPanelResizerProps & { id?: string };
31
+
32
+ const {
33
+ size = "0px",
34
+ id: _id,
35
+ onDragStart,
36
+ onDrag,
37
+ onDragEnd,
38
+ disabled,
39
+ ...attrs
40
+ } = defineProps<PanelResizerProps>();
41
+
42
+ const defaultId = _id || useId();
43
+ const id = defaultId;
44
+
45
+ const send = inject<SendFn>("send");
46
+ const state = inject<Ref<GroupMachineContextValue>>("state");
47
+ const isPrerender = inject<Ref<boolean>>("isPrerender");
48
+
49
+ const initHandle = () => initializePanelHandleData({ size, id });
50
+ const handleData = computed(() => {
51
+ const item = state?.value?.items.find((i) => i.id === id);
52
+ if (!item || !isPanelHandle(item)) return undefined;
53
+ return item;
54
+ });
55
+
56
+ let dynamicPanelHandleIsMounting = false;
57
+
58
+ if (!handleData.value) {
59
+ if (isPrerender?.value) {
60
+ send?.({ type: "registerPanelHandle", data: initHandle() });
61
+ } else {
62
+ dynamicPanelHandleIsMounting = true;
63
+ }
64
+ }
65
+
66
+ onMounted(() => {
67
+ const groupId = state?.value?.groupId;
68
+ if (!groupId || !dynamicPanelHandleIsMounting) return;
69
+
70
+ const groupElement = document.getElementById(state?.value?.groupId);
71
+ if (!groupElement) return;
72
+
73
+ const handleEl = document.getElementById(id);
74
+ if (!handleEl) return;
75
+
76
+ const order = Array.from(groupElement.children).indexOf(handleEl);
77
+ if (typeof order !== "number") return;
78
+
79
+ send?.({
80
+ type: "registerPanelHandle",
81
+ data: { ...initHandle(), order },
82
+ });
83
+ dynamicPanelHandleIsMounting = false;
84
+ });
85
+
86
+ const contraintChanged = computed(
87
+ () =>
88
+ !dynamicPanelHandleIsMounting &&
89
+ haveConstraintsChangedForPanelHandle(initHandle(), handleData.value),
90
+ );
91
+
92
+ watchEffect(() => {
93
+ if (!contraintChanged.value) return;
94
+ send?.({ type: "updateConstraints", data: initHandle() });
95
+ });
96
+
97
+ onUnmounted(() => {
98
+ requestAnimationFrame(() => send?.({ type: "unregisterPanelHandle", id }));
99
+ });
100
+
101
+ const { moveProps } = move({
102
+ onMoveStart: () => {
103
+ send?.({ type: "dragHandleStart", handleId: id });
104
+ onDragStart?.();
105
+ if (!state?.value) return;
106
+ document.body.style.cursor = getCursor(state?.value) || "auto";
107
+ },
108
+ onMove: (e) => {
109
+ send?.({ type: "dragHandle", handleId: id, value: e });
110
+ onDrag?.();
111
+ },
112
+ onMoveEnd: () => {
113
+ send?.({ type: "dragHandleEnd", handleId: id });
114
+ onDragEnd?.();
115
+ document.body.style.cursor = "auto";
116
+ },
117
+ });
118
+
119
+ const onKeyDown = (e: KeyboardEvent) => {
120
+ if (!state?.value) return;
121
+
122
+ try {
123
+ const collapsiblePanel = getCollapsiblePanelForHandleId(state?.value, id);
124
+
125
+ if (e.key === "Enter" && collapsiblePanel) {
126
+ if (collapsiblePanel.collapsed) {
127
+ send?.({ type: "expandPanel", panelId: collapsiblePanel.id });
128
+ } else {
129
+ send?.({ type: "collapsePanel", panelId: collapsiblePanel.id });
130
+ }
131
+ }
132
+ } catch {
133
+ //
134
+ }
135
+ };
136
+
137
+ const combinedProps = computed(() => {
138
+ const handleIndex = state?.value?.items.findIndex((item) => item.id === id);
139
+
140
+ if (!handleIndex) return { id };
141
+
142
+ const handle = state?.value?.items[handleIndex] as PanelHandleData;
143
+ const panelBeforeHandle = state?.value?.items[handleIndex - 1];
144
+
145
+ if (!panelBeforeHandle || !isPanelData(panelBeforeHandle)) return { id };
146
+
147
+ return {
148
+ ...attrs,
149
+ ...getPanelResizerDomAttributes({
150
+ groupId: state?.value?.groupId,
151
+ id,
152
+ orientation: state?.value?.orientation || "horizontal",
153
+ isDragging: state?.value?.activeDragHandleId === id,
154
+ activeDragHandleId: state?.value?.activeDragHandleId,
155
+ disabled: disabled,
156
+ controlsId: panelBeforeHandle.id,
157
+ min: panelBeforeHandle.min,
158
+ max: panelBeforeHandle.max,
159
+ currentValue: panelBeforeHandle.currentValue,
160
+ groupSize: getGroupSize(state.value),
161
+ }),
162
+ onpointerdown: disabled ? undefined : moveProps.onPointerDown,
163
+ onkeydown: disabled
164
+ ? undefined
165
+ : (e: KeyboardEvent) => {
166
+ moveProps.onKeyDown(e);
167
+ onKeyDown(e);
168
+ },
169
+ style: {
170
+ cursor: state?.value ? getCursor(state.value) : undefined,
171
+ width:
172
+ state?.value?.orientation === "horizontal"
173
+ ? `${handle?.size.value.toNumber()}px`
174
+ : "100%",
175
+ height:
176
+ state?.value?.orientation === "vertical"
177
+ ? `${handle?.size.value.toNumber()}px`
178
+ : "100%",
179
+ },
180
+ };
181
+ });
182
+ </script>
183
+
184
+ <template>
185
+ <div v-bind="combinedProps" data-panel-resizer :tabindex="disabled ? -1 : 0">
186
+ <slot />
187
+ </div>
188
+ </template>