Skip to content

Commit e889ed8

Browse files
refactor: move component logic to composables (#36)
1 parent a11b4b6 commit e889ed8

File tree

7 files changed

+392
-218
lines changed

7 files changed

+392
-218
lines changed

packages/vue-split-panel/src/SplitPanel.vue

Lines changed: 66 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script lang="ts" setup>
22
import type { SplitPanelProps } from './types';
3-
import { clamp, useDraggable, useElementSize, useResizeObserver } from '@vueuse/core';
4-
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
5-
import { closestNumber } from './utils/closest-number';
6-
import { percentageToPixels } from './utils/percentage-to-pixels';
7-
import { pixelsToPercentage } from './utils/pixels-to-percentage';
3+
import { ref, useTemplateRef, watch } from 'vue';
4+
import { useGridTemplate } from './composables/use-grid-template';
5+
import { useKeyboard } from './composables/use-keyboard';
6+
import { usePointer } from './composables/use-pointer';
7+
import { useResize } from './composables/use-resize';
8+
import { useSizes } from './composables/use-sizes';
89
910
const props = withDefaults(defineProps<SplitPanelProps>(), {
1011
orientation: 'horizontal',
@@ -25,80 +26,80 @@ const emits = defineEmits<{
2526
transitionend: [event: TransitionEvent];
2627
}>();
2728
28-
const panelEl = useTemplateRef('split-panel');
29-
const dividerEl = useTemplateRef('divider');
30-
31-
const { width: componentWidth, height: componentHeight } = useElementSize(panelEl);
32-
const componentSize = computed(() => props.orientation === 'horizontal' ? componentWidth.value : componentHeight.value);
33-
34-
const { width: dividerWidth, height: dividerHeight } = useElementSize(dividerEl);
35-
const dividerSize = computed(() => props.orientation === 'horizontal' ? dividerWidth.value : dividerHeight.value);
36-
3729
/** Size of the primary panel in either percentages or pixels as defined by the sizeUnit property */
3830
const size = defineModel<number>('size', { default: 50 });
3931
40-
const sizePercentage = computed({
41-
get() {
42-
if (props.sizeUnit === '%') return size.value;
43-
return pixelsToPercentage(componentSize.value, size.value);
44-
},
45-
set(newValue: number) {
46-
if (props.sizeUnit === '%') {
47-
size.value = newValue;
48-
}
49-
else {
50-
size.value = percentageToPixels(componentSize.value, newValue);
51-
}
52-
},
53-
});
54-
55-
const sizePixels = computed({
56-
get() {
57-
if (props.sizeUnit === 'px') return size.value;
58-
return percentageToPixels(componentSize.value, size.value);
59-
},
60-
set(newValue: number) {
61-
if (props.sizeUnit === 'px') {
62-
size.value = newValue;
63-
}
64-
else {
65-
size.value = pixelsToPercentage(componentSize.value, newValue);
66-
}
67-
},
68-
});
32+
/** Whether the primary column is collapsed or not */
33+
const collapsed = defineModel<boolean>('collapsed', { default: false });
6934
70-
const minSizePercentage = computed(() => {
71-
if (props.minSize === undefined) return;
35+
const panelEl = useTemplateRef('split-panel');
36+
const dividerEl = useTemplateRef('divider');
7237
73-
if (props.sizeUnit === '%') return props.minSize;
74-
return pixelsToPercentage(componentSize.value, props.minSize);
75-
});
38+
let expandedSizePercentage = 0;
7639
77-
const minSizePixels = computed(() => {
78-
if (props.minSize === undefined) return;
40+
const collapseTransitionState = ref<null | 'expanding' | 'collapsing'>(null);
7941
80-
if (props.sizeUnit === 'px') return props.minSize;
81-
return percentageToPixels(componentSize.value, props.minSize);
42+
const {
43+
sizePercentage,
44+
sizePixels,
45+
maxSizePercentage,
46+
minSizePercentage,
47+
minSizePixels,
48+
componentSize,
49+
dividerSize,
50+
snapPixels,
51+
} = useSizes(size, {
52+
disabled: () => props.disabled,
53+
collapsible: () => props.collapsible,
54+
primary: () => props.primary,
55+
orientation: () => props.orientation,
56+
sizeUnit: () => props.sizeUnit,
57+
minSize: () => props.minSize,
58+
maxSize: () => props.maxSize,
59+
snapPoints: () => props.snapPoints,
60+
panelEl,
61+
dividerEl,
8262
});
8363
84-
const maxSizePercentage = computed(() => {
85-
if (props.maxSize === undefined) return;
86-
87-
if (props.sizeUnit === '%') return props.maxSize;
88-
return pixelsToPercentage(componentSize.value, props.maxSize);
64+
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, {
65+
disabled: () => props.disabled,
66+
collapsible: () => props.collapsible,
67+
primary: () => props.primary,
68+
orientation: () => props.orientation,
8969
});
9070
91-
const snapPixels = computed(() => {
92-
if (props.sizeUnit === 'px') return props.snapPoints;
93-
return props.snapPoints.map((snapPercentage) => percentageToPixels(componentSize.value, snapPercentage));
71+
const { isDragging, handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, {
72+
collapseThreshold: () => props.collapseThreshold,
73+
collapsible: () => props.collapsible,
74+
direction: () => props.direction,
75+
disabled: () => props.disabled,
76+
orientation: () => props.orientation,
77+
primary: () => props.primary,
78+
snapThreshold: () => props.snapThreshold,
79+
panelEl,
80+
dividerEl,
81+
minSizePixels,
82+
componentSize,
83+
snapPixels,
9484
});
9585
96-
let expandedSizePercentage = 0;
97-
98-
/** Whether the primary column is collapsed or not */
99-
const collapsed = defineModel<boolean>('collapsed', { default: false });
86+
const { gridTemplate } = useGridTemplate({
87+
collapsed,
88+
direction: () => props.direction,
89+
dividerSize,
90+
maxSizePercentage,
91+
minSizePercentage,
92+
orientation: () => props.orientation,
93+
primary: () => props.primary,
94+
sizePercentage,
95+
});
10096
101-
const collapseTransitionState = ref<null | 'expanding' | 'collapsing'>(null);
97+
useResize(sizePercentage, {
98+
sizePixels,
99+
panelEl,
100+
orientation: () => props.orientation,
101+
primary: () => props.primary,
102+
});
102103
103104
const onTransitionEnd = (event: TransitionEvent) => {
104105
collapseTransitionState.value = null;
@@ -117,155 +118,6 @@ watch(collapsed, (newCollapsed) => {
117118
}
118119
});
119120
120-
let cachedSizePixels = 0;
121-
122-
onMounted(() => {
123-
cachedSizePixels = sizePixels.value;
124-
});
125-
126-
const { x: dividerX, y: dividerY, isDragging } = useDraggable(dividerEl, { containerElement: panelEl });
127-
128-
let hasToggledDuringCurrentDrag = false;
129-
130-
watch([dividerX, dividerY], ([newX, newY]) => {
131-
if (props.disabled) return;
132-
133-
let newPositionInPixels = props.orientation === 'horizontal' ? newX : newY;
134-
135-
if (props.primary === 'end') {
136-
newPositionInPixels = componentSize.value - newPositionInPixels;
137-
}
138-
139-
if (props.collapsible && minSizePixels.value !== undefined && props.collapseThreshold !== undefined && hasToggledDuringCurrentDrag === false) {
140-
const collapseThreshold = minSizePixels.value - (props.collapseThreshold ?? 0);
141-
const expandThreshold = (props.collapseThreshold ?? 0);
142-
143-
if (newPositionInPixels < collapseThreshold && collapsed.value === false) {
144-
collapsed.value = true;
145-
hasToggledDuringCurrentDrag = true;
146-
}
147-
else if (newPositionInPixels > expandThreshold && collapsed.value === true) {
148-
collapsed.value = false;
149-
hasToggledDuringCurrentDrag = true;
150-
}
151-
}
152-
153-
for (let snapPoint of snapPixels.value) {
154-
if (props.direction === 'rtl' && props.orientation === 'horizontal') {
155-
snapPoint = componentSize.value - snapPoint;
156-
}
157-
158-
if (
159-
newPositionInPixels >= snapPoint - props.snapThreshold
160-
&& newPositionInPixels <= snapPoint + props.snapThreshold
161-
) {
162-
newPositionInPixels = snapPoint;
163-
}
164-
}
165-
166-
sizePercentage.value = clamp(pixelsToPercentage(componentSize.value, newPositionInPixels), 0, 100);
167-
});
168-
169-
watch(isDragging, (newDragging) => {
170-
if (newDragging === false) hasToggledDuringCurrentDrag = false;
171-
});
172-
173-
watch(sizePixels, (newPixels, oldPixels) => {
174-
if (newPixels === oldPixels) return;
175-
cachedSizePixels = newPixels;
176-
});
177-
178-
useResizeObserver(panelEl, (entries) => {
179-
const entry = entries[0];
180-
const { width, height } = entry.contentRect;
181-
const size = props.orientation === 'horizontal' ? width : height;
182-
183-
if (props.primary) {
184-
sizePercentage.value = pixelsToPercentage(size, cachedSizePixels);
185-
}
186-
});
187-
188-
const handleKeydown = (event: KeyboardEvent) => {
189-
if (props.disabled) return;
190-
191-
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'].includes(event.key)) {
192-
event.preventDefault();
193-
194-
let newPosition = sizePercentage.value;
195-
196-
const increment = (event.shiftKey ? 10 : 1) * (props.primary === 'end' ? -1 : 1);
197-
198-
if (
199-
(event.key === 'ArrowLeft' && props.orientation === 'horizontal')
200-
|| (event.key === 'ArrowUp' && props.orientation === 'vertical')
201-
) {
202-
newPosition -= increment;
203-
}
204-
205-
if (
206-
(event.key === 'ArrowRight' && props.orientation === 'horizontal')
207-
|| (event.key === 'ArrowDown' && props.orientation === 'vertical')
208-
) {
209-
newPosition += increment;
210-
}
211-
212-
if (event.key === 'Home') {
213-
newPosition = props.primary === 'end' ? 100 : 0;
214-
}
215-
216-
if (event.key === 'End') {
217-
newPosition = props.primary === 'end' ? 0 : 100;
218-
}
219-
220-
if (event.key === 'Enter' && props.collapsible) {
221-
collapsed.value = !collapsed.value;
222-
}
223-
224-
sizePercentage.value = clamp(newPosition, 0, 100);
225-
}
226-
};
227-
228-
const handleDblClick = () => {
229-
const closest = closestNumber(snapPixels.value, sizePixels.value);
230-
231-
if (closest !== undefined) {
232-
sizePixels.value = closest;
233-
}
234-
};
235-
236-
const gridTemplate = computed(() => {
237-
let primary: string;
238-
239-
if (collapsed.value) {
240-
primary = '0';
241-
}
242-
else if (minSizePercentage.value !== undefined && maxSizePercentage.value !== undefined) {
243-
primary = `clamp(0%, clamp(${minSizePercentage.value}%, ${sizePercentage.value}%, ${maxSizePercentage.value}%), calc(100% - ${dividerSize.value}px))`;
244-
}
245-
else {
246-
primary = `clamp(0%, ${sizePercentage.value}%, calc(100% - ${dividerSize.value}px))`;
247-
}
248-
249-
const secondary = 'auto';
250-
251-
if (!props.primary || props.primary === 'start') {
252-
if (props.direction === 'ltr' || props.orientation === 'vertical') {
253-
return `${primary} ${dividerSize.value}px ${secondary}`;
254-
}
255-
else {
256-
return `${secondary} ${dividerSize.value}px ${primary}`;
257-
}
258-
}
259-
else {
260-
if (props.direction === 'ltr' || props.orientation === 'vertical') {
261-
return `${secondary} ${dividerSize.value}px ${primary}`;
262-
}
263-
else {
264-
return `${primary} ${dividerSize.value}px ${secondary}`;
265-
}
266-
}
267-
});
268-
269121
const collapse = () => collapsed.value = true;
270122
const expand = () => collapsed.value = false;
271123
const toggle = (val: boolean) => collapsed.value = val;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
2+
import type { Direction, Orientation, Primary } from '../types';
3+
import { computed, toValue } from 'vue';
4+
5+
export interface UseGridTemplateOptions {
6+
collapsed: Ref<boolean>;
7+
minSizePercentage: ComputedRef<number | undefined>;
8+
maxSizePercentage: ComputedRef<number | undefined>;
9+
sizePercentage: ComputedRef<number>;
10+
dividerSize: ComputedRef<number>;
11+
primary: MaybeRefOrGetter<Primary | undefined>;
12+
direction: MaybeRefOrGetter<Direction>;
13+
orientation: MaybeRefOrGetter<Orientation>;
14+
}
15+
16+
export const useGridTemplate = (options: UseGridTemplateOptions) => {
17+
const gridTemplate = computed(() => {
18+
let primary: string;
19+
20+
if (options.collapsed.value) {
21+
primary = '0';
22+
}
23+
else if (options.minSizePercentage.value !== undefined && options.maxSizePercentage.value !== undefined) {
24+
primary = `clamp(0%, clamp(${options.minSizePercentage.value}%, ${options.sizePercentage.value}%, ${options.maxSizePercentage.value}%), calc(100% - ${options.dividerSize.value}px))`;
25+
}
26+
else {
27+
primary = `clamp(0%, ${options.sizePercentage.value}%, calc(100% - ${options.dividerSize.value}px))`;
28+
}
29+
30+
const secondary = 'auto';
31+
32+
if (!toValue(options.primary) || toValue(options.primary) === 'start') {
33+
if (toValue(options.direction) === 'ltr' || toValue(options.orientation) === 'vertical') {
34+
return `${primary} ${options.dividerSize.value}px ${secondary}`;
35+
}
36+
else {
37+
return `${secondary} ${options.dividerSize.value}px ${primary}`;
38+
}
39+
}
40+
else {
41+
if (toValue(options.direction) === 'ltr' || toValue(options.orientation) === 'vertical') {
42+
return `${secondary} ${options.dividerSize.value}px ${primary}`;
43+
}
44+
else {
45+
return `${primary} ${options.dividerSize.value}px ${secondary}`;
46+
}
47+
}
48+
});
49+
50+
return { gridTemplate };
51+
};

0 commit comments

Comments
 (0)