|
<script lang="ts"> |
|
import type { PropType } from 'vue' |
|
import { computed, defineComponent, ref, watch } from 'vue' |
|
import { useLocalStorage } from '@vueuse/core' |
|
import { useOrientation } from './orientation' |
|
|
|
export default defineComponent({ |
|
props: { |
|
defaultSplit: { |
|
type: Number, |
|
default: 50, |
|
}, |
|
|
|
min: { |
|
type: Number, |
|
default: 20, |
|
}, |
|
|
|
max: { |
|
type: Number, |
|
default: 80, |
|
}, |
|
|
|
draggerOffset: { |
|
type: String as PropType<'before' | 'center' | 'after'>, |
|
default: 'center', |
|
validator: (value: string) => ['before', 'center', 'after'].includes(value), |
|
}, |
|
|
|
saveId: { |
|
type: String, |
|
default: null, |
|
}, |
|
|
|
collapsableLeft: { |
|
type: Boolean, |
|
default: false, |
|
}, |
|
|
|
collapsableRight: { |
|
type: Boolean, |
|
default: false, |
|
}, |
|
}, |
|
|
|
emits: ['leftCollapsed', 'rightCollapsed'], |
|
|
|
setup(props, { emit }) { |
|
const { orientation } = useOrientation() |
|
|
|
const split = props.saveId ? useLocalStorage(`split-pane-${props.saveId}`, props.defaultSplit) : ref(props.defaultSplit) |
|
const leftCollapsed = props.saveId ? useLocalStorage(`split-pane-collapsed-left-${props.saveId}`, false) : ref(false) |
|
const rightCollapsed = props.saveId ? useLocalStorage(`split-pane-collapsed-right-${props.saveId}`, false) : ref(false) |
|
|
|
watch(leftCollapsed, (value) => { |
|
emit('leftCollapsed', value) |
|
}, { |
|
immediate: true, |
|
}) |
|
|
|
watch(rightCollapsed, (value) => { |
|
emit('rightCollapsed', value) |
|
}, { |
|
immediate: true, |
|
}) |
|
|
|
const boundSplit = computed(() => { |
|
if (split.value < props.min) { |
|
return props.min |
|
} |
|
else if (split.value > props.max) { |
|
return props.max |
|
} |
|
else { |
|
return split.value |
|
} |
|
}) |
|
|
|
const leftStyle = computed(() => ({ |
|
[orientation.value === 'landscape' ? 'width' : 'height']: `${leftCollapsed.value ? 0 : rightCollapsed.value ? 100 : boundSplit.value}%`, |
|
})) |
|
|
|
const rightStyle = computed(() => ({ |
|
[orientation.value === 'landscape' ? 'width' : 'height']: `${rightCollapsed.value ? 0 : leftCollapsed.value ? 100 : 100 - boundSplit.value}%`, |
|
})) |
|
|
|
const dragging = ref(false) |
|
let startPosition = 0 |
|
let startSplit = 0 |
|
const el = ref(null) |
|
|
|
function dragStart(e: MouseEvent) { |
|
dragging.value = true |
|
startPosition = orientation.value === 'landscape' ? e.pageX : e.pageY |
|
startSplit = boundSplit.value |
|
} |
|
|
|
function dragMove(e: MouseEvent) { |
|
if (dragging.value) { |
|
let position |
|
let totalSize |
|
if (orientation.value === 'landscape') { |
|
position = e.pageX |
|
totalSize = el.value.offsetWidth |
|
} |
|
else { |
|
position = e.pageY |
|
totalSize = el.value.offsetHeight |
|
} |
|
const dPosition = position - startPosition |
|
split.value = startSplit + ~~(dPosition / totalSize * 100) |
|
} |
|
} |
|
|
|
function dragEnd() { |
|
dragging.value = false |
|
} |
|
|
|
|
|
|
|
function collapseLeft() { |
|
if (rightCollapsed.value) { |
|
rightCollapsed.value = false |
|
} |
|
else { |
|
leftCollapsed.value = true |
|
} |
|
} |
|
|
|
function collapseRight() { |
|
if (leftCollapsed.value) { |
|
leftCollapsed.value = false |
|
} |
|
else { |
|
rightCollapsed.value = true |
|
} |
|
} |
|
|
|
return { |
|
el, |
|
dragging, |
|
orientation, |
|
dragStart, |
|
dragMove, |
|
dragEnd, |
|
leftStyle, |
|
rightStyle, |
|
leftCollapsed, |
|
rightCollapsed, |
|
collapseLeft, |
|
collapseRight, |
|
} |
|
}, |
|
}) |
|
</script> |
|
|
|
<template> |
|
<div |
|
ref="el" |
|
class="flex h-full" |
|
:class="{ |
|
'flex-col meow': orientation === 'portrait', |
|
'cursor-ew-resize': dragging && orientation === 'landscape', |
|
'cursor-ns-resize': dragging && orientation === 'portrait', |
|
[orientation]: true, |
|
}" |
|
@mousemove="dragMove" |
|
@mouseup="dragEnd" |
|
@mouseleave="dragEnd" |
|
> |
|
<div |
|
class="relative top-0 left-0 overflow-hidden" |
|
:class="{ |
|
'pointer-events-none': dragging, |
|
'border-r border-gray-200 dark:border-gray-700': orientation === 'landscape' && !leftCollapsed && !rightCollapsed, |
|
}" |
|
:style="leftStyle" |
|
> |
|
<slot |
|
v-if="!leftCollapsed" |
|
name="left" |
|
/> |
|
|
|
<div |
|
v-if="!leftCollapsed && !rightCollapsed" |
|
class="dragger absolute z-100 hover:bg-green-500 hover:bg-opacity-25 transition-colors duration-150 delay-150" |
|
:class="{ |
|
'top-0 bottom-0 cursor-ew-resize': orientation === 'landscape', |
|
'left-0 right-0 cursor-ns-resize': orientation === 'portrait', |
|
[`dragger-offset-${draggerOffset}`]: true, |
|
}" |
|
@mousedown.prevent="dragStart" |
|
/> |
|
|
|
<div |
|
v-if="(collapsableLeft && !leftCollapsed) || rightCollapsed" |
|
class="collapse-btn absolute z-[101] flex items-center pointer-events-none" |
|
:class="{ |
|
'top-0 bottom-0 right-0': orientation === 'landscape', |
|
'left-0 right-0 bottom-0 flex-col': orientation === 'portrait', |
|
}" |
|
> |
|
<button |
|
v-tooltip="rightCollapsed ? `Expand ${orientation === 'landscape' ? 'Right' : 'Bottom'} pane` : `Collapse ${orientation === 'landscape' ? 'Left' : 'Top'} pane`" |
|
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 flex items-center justify-center w-2.5 h-6 rounded overflow-hidden pointer-events-auto opacity-70 hover:opacity-100" |
|
:class="{ |
|
'rounded-r-none border-r-0': orientation === 'landscape', |
|
'rounded-b-none border-b-0': orientation === 'portrait', |
|
}" |
|
@click="collapseLeft()" |
|
> |
|
<VueIcon |
|
:icon="orientation === 'landscape' ? 'arrow_left' : 'arrow_drop_up'" |
|
class="flex-none w-5 h-5" |
|
/> |
|
</button> |
|
</div> |
|
</div> |
|
<div |
|
class="relative bottom-0 right-0" |
|
:class="{ |
|
'pointer-events-none': dragging, |
|
'border-t border-gray-200 dark:border-gray-700': orientation === 'portrait', |
|
}" |
|
:style="rightStyle" |
|
> |
|
<div |
|
v-if="(collapsableRight && !rightCollapsed) || leftCollapsed" |
|
class="collapse-btn absolute z-[101] flex items-center pointer-events-none" |
|
:class="{ |
|
'top-0 bottom-0 left-0': orientation === 'landscape', |
|
'left-0 right-0 top-0 flex-col': orientation === 'portrait', |
|
}" |
|
> |
|
<button |
|
v-tooltip="leftCollapsed ? `Expand ${orientation === 'landscape' ? 'Left' : 'Top'} pane` : `Collapse ${orientation === 'landscape' ? 'Right' : 'Bottom'} pane`" |
|
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 flex items-center justify-center w-2.5 h-6 rounded overflow-hidden pointer-events-auto opacity-70 hover:opacity-100" |
|
:class="{ |
|
'rounded-l-none border-l-0': orientation === 'landscape', |
|
'rounded-t-none border-t-0': orientation === 'portrait', |
|
}" |
|
@click="collapseRight()" |
|
> |
|
<VueIcon |
|
:icon="orientation === 'landscape' ? 'arrow_right' : 'arrow_drop_down'" |
|
class="flex-none w-5 h-5" |
|
/> |
|
</button> |
|
</div> |
|
|
|
<slot |
|
v-if="!rightCollapsed" |
|
name="right" |
|
/> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<style lang="postcss" scoped> |
|
.dragger { |
|
.landscape & { |
|
width: 10px; |
|
} |
|
|
|
.portrait & { |
|
height: 10px; |
|
} |
|
|
|
&.dragger-offset-before { |
|
.landscape & { |
|
right: 0; |
|
} |
|
|
|
.portrait & { |
|
bottom: 0; |
|
} |
|
} |
|
|
|
&.dragger-offset-center { |
|
.landscape & { |
|
right: -5px; |
|
} |
|
|
|
.portrait & { |
|
bottom: -5px; |
|
} |
|
} |
|
|
|
&.dragger-offset-after { |
|
.landscape & { |
|
right: -10px; |
|
} |
|
|
|
.portrait & { |
|
bottom: -10px; |
|
} |
|
} |
|
} |
|
|
|
.collapse-btn { |
|
button { |
|
.portrait & { |
|
@apply w-6 h-2.5; |
|
} |
|
} |
|
} |
|
</style> |
|
|