153 lines
3.9 KiB
Vue
153 lines
3.9 KiB
Vue
<template>
|
|
<q-scroll-area
|
|
ref="scrollAreaRef"
|
|
:style="scrollStyle"
|
|
:vertical-offset="verticalOffset"
|
|
:thumb-style="thumbStyle"
|
|
@scroll="onScroll"
|
|
class="shadow-scroll position-relative w100"
|
|
>
|
|
<q-resize-observer @resize="onInnerResize" />
|
|
|
|
<div :style="topSpacerStyle" />
|
|
<slot />
|
|
<div :style="{ height: bottom + 'px' }" />
|
|
</q-scroll-area>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { QScrollArea } from 'quasar'
|
|
|
|
interface Props {
|
|
height: number
|
|
topOffset?: number
|
|
bottomOffset?: number
|
|
scrollPosition?: number
|
|
offset?: number
|
|
scrollHeaderSizeFull?: number
|
|
scrollHeaderSizeSticky?: number
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
scrollPosition: 0,
|
|
offset: 16,
|
|
scrollHeaderSizeFull: 0,
|
|
scrollHeaderSizeSticky: 0
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'updateScrollPosition',
|
|
'resizeScrollArea'
|
|
])
|
|
|
|
const scrollAreaRef = ref<QScrollArea | null>(null)
|
|
const currentScroll = ref(0)
|
|
const innerHeight = ref(0)
|
|
|
|
const top = computed(() => props.topOffset || props.offset)
|
|
const bottom = computed(() => props.bottomOffset || props.offset)
|
|
|
|
const offsetBefore = computed(() => {
|
|
if (props.scrollHeaderSizeFull && props.scrollHeaderSizeSticky) {
|
|
return Math.max(
|
|
props.scrollHeaderSizeFull - currentScroll.value,
|
|
props.scrollHeaderSizeSticky
|
|
)
|
|
}
|
|
return 0
|
|
})
|
|
|
|
const verticalOffset = computed<[number, number]>(() => [
|
|
top.value + offsetBefore.value,
|
|
bottom.value
|
|
])
|
|
|
|
const thumbStyle = {
|
|
borderRadius: '3px',
|
|
background: '#999',
|
|
width: '6px',
|
|
opacity: '0.4'
|
|
}
|
|
|
|
const scrollStyle = computed(() => {
|
|
const isHeaderSticky = props.scrollHeaderSizeFull && props.scrollHeaderSizeSticky
|
|
? (props.scrollHeaderSizeFull - currentScroll.value <= props.scrollHeaderSizeSticky)
|
|
: true
|
|
|
|
return {
|
|
height: props.height + 'px',
|
|
'--top-height': top.value + 'px',
|
|
'--bottom-height': bottom.value + 'px',
|
|
'--offset-before': offsetBefore.value + 'px',
|
|
'--top-shadow-visibility': isHeaderSticky ? 'visible' : 'hidden'
|
|
}
|
|
})
|
|
|
|
const topSpacerStyle = computed(() => ({
|
|
height: top.value + 'px',
|
|
visibility: offsetBefore.value === 0 ? 'visible' as const : 'hidden' as const,
|
|
marginTop: offsetBefore.value === 0 ? '0px' : `-${top.value}px`
|
|
}))
|
|
|
|
function onScroll ({ verticalPosition }: { verticalPosition: number }) {
|
|
currentScroll.value = verticalPosition
|
|
emit('updateScrollPosition', verticalPosition)
|
|
}
|
|
|
|
function onInnerResize (size: { height: number }) {
|
|
innerHeight.value = size.height
|
|
emit('resizeScrollArea', {
|
|
height: props.height,
|
|
innerHeight: size.height,
|
|
topOffset: top.value,
|
|
bottomOffset: bottom.value
|
|
})
|
|
}
|
|
|
|
watch(() => props.scrollPosition, (newPos) => {
|
|
if (newPos !== currentScroll.value) {
|
|
scrollAreaRef.value?.setScrollPosition('vertical', newPos)
|
|
}
|
|
})
|
|
|
|
watch([top, bottom, () => props.height], () => {
|
|
emit('resizeScrollArea', {
|
|
height: props.height,
|
|
innerHeight: innerHeight.value,
|
|
topOffset: top.value,
|
|
bottomOffset: bottom.value
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.shadow-scroll:before,
|
|
.shadow-scroll:after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
width: 100%;
|
|
pointer-events: none;
|
|
z-index: 95;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.shadow-scroll:before {
|
|
top: var(--offset-before);
|
|
height: var(--top-height);
|
|
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
|
visibility: var(--top-shadow-visibility);
|
|
}
|
|
|
|
.shadow-scroll:after {
|
|
bottom: 0;
|
|
height: var(--bottom-height);
|
|
background: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
|
}
|
|
|
|
.shadow-scroll :deep(.q-scrollarea__content) {
|
|
max-width: 100%;
|
|
}
|
|
</style>
|