opnform-host-nginx/client/components/global/ScrollShadow.vue

210 lines
5.1 KiB
Vue
Raw Normal View History

2023-12-09 15:47:03 +01:00
<template>
<div
class="scroll-shadow max-w-full"
:class="[$style.wrap, { 'w-max': !shadow.left && !shadow.right }]"
>
2023-12-09 15:47:03 +01:00
<div
ref="scrollContainer"
:class="[$style['scroll-container'], { 'no-scrollbar': hideScrollbar }]"
:style="{ width: width ? width : 'auto', height }"
2024-02-10 12:20:45 +01:00
@scroll.passive="throttled.toggleShadow"
2023-12-09 15:47:03 +01:00
>
<slot />
<span
:class="[$style['shadow-top'], shadow.top && $style['is-active']]"
:style="{
top: shadowTopOffset + 'px',
}"
/>
<span
:class="[$style['shadow-right'], shadow.right && $style['is-active']]"
/>
<span
:class="[$style['shadow-bottom'], shadow.bottom && $style['is-active']]"
/>
<span
:class="[$style['shadow-left'], shadow.left && $style['is-active']]"
2023-12-09 15:47:03 +01:00
/>
</div>
</div>
</template>
<script>
import throttle from "lodash/throttle"
function newResizeObserver(callback) {
2023-12-09 15:47:03 +01:00
// Skip this feature for browsers which
// do not support ResizeObserver.
// https://caniuse.com/#search=resizeobserver
if (typeof ResizeObserver === "undefined") return
2023-12-09 15:47:03 +01:00
return new ResizeObserver((e) => e.map(callback))
2023-12-09 15:47:03 +01:00
}
export default {
name: "ScrollShadow",
2023-12-09 15:47:03 +01:00
props: {
hideScrollbar: {
type: Boolean,
default: false,
2023-12-09 15:47:03 +01:00
},
shadowTopOffset: {
type: Number,
default: 0,
},
2023-12-09 15:47:03 +01:00
},
data() {
2023-12-09 15:47:03 +01:00
return {
width: undefined,
height: undefined,
shadow: {
top: false,
right: false,
bottom: false,
left: false,
2023-12-09 15:47:03 +01:00
},
scrollContainerObserver: null,
2024-02-10 12:20:45 +01:00
wrapObserver: null,
throttled: {},
2023-12-09 15:47:03 +01:00
}
},
mounted() {
this.throttled.toggleShadow = throttle(this.toggleShadow, 100)
this.throttled.calcDimensions = throttle(this.calcDimensions, 100)
2024-02-10 12:20:45 +01:00
window.addEventListener("resize", this.throttled.calcDimensions)
2023-12-09 15:47:03 +01:00
// Check if shadows are necessary after the element is resized.
const scrollContainerObserver = newResizeObserver(
this.throttled.toggleShadow,
)
2023-12-09 15:47:03 +01:00
if (scrollContainerObserver) {
scrollContainerObserver.observe(this.$refs.scrollContainer)
}
// Recalculate the container dimensions when the wrapper is resized.
2024-02-10 12:20:45 +01:00
this.wrapObserver = newResizeObserver(this.throttled.calcDimensions)
2023-12-09 15:47:03 +01:00
if (this.wrapObserver) {
this.wrapObserver.observe(this.$el)
}
},
unmounted() {
window.removeEventListener("resize", this.throttled.calcDimensions)
2023-12-09 15:47:03 +01:00
// Cleanup when the component is unmounted.
this.wrapObserver.disconnect()
2023-12-24 09:51:22 +01:00
if (this.scrollContainerObserver) {
this.scrollContainerObserver.disconnect()
}
2023-12-09 15:47:03 +01:00
},
methods: {
async calcDimensions() {
2023-12-09 15:47:03 +01:00
// Reset dimensions for correctly recalculating parent dimensions.
this.width = undefined
this.height = undefined
await this.$nextTick()
this.width = `${this.$el.clientWidth}px`
this.height = `${this.$el.clientHeight}px`
},
// Check if shadows are needed.
toggleShadow() {
2023-12-24 09:51:22 +01:00
if (!this.$refs.scrollContainer) return
2023-12-09 15:47:03 +01:00
const hasHorizontalScrollbar =
this.$refs.scrollContainer.clientWidth <
this.$refs.scrollContainer.scrollWidth
const hasVerticalScrollbar =
this.$refs.scrollContainer.clientHeight <
this.$refs.scrollContainer.scrollHeight
const scrolledFromLeft =
this.$refs.scrollContainer.offsetWidth +
this.$refs.scrollContainer.scrollLeft
const scrolledFromTop =
this.$refs.scrollContainer.offsetHeight +
this.$refs.scrollContainer.scrollTop
const scrolledToTop = this.$refs.scrollContainer.scrollTop === 0
const scrolledToRight =
scrolledFromLeft >= this.$refs.scrollContainer.scrollWidth
const scrolledToBottom =
scrolledFromTop >= this.$refs.scrollContainer.scrollHeight
const scrolledToLeft = this.$refs.scrollContainer.scrollLeft === 0
this.$nextTick(() => {
this.shadow.top = hasVerticalScrollbar && !scrolledToTop
this.shadow.right = hasHorizontalScrollbar && !scrolledToRight
this.shadow.bottom = hasVerticalScrollbar && !scrolledToBottom
this.shadow.left = hasHorizontalScrollbar && !scrolledToLeft
})
},
},
2023-12-09 15:47:03 +01:00
}
</script>
<style lang="scss" module>
.wrap {
overflow: hidden;
position: relative;
}
.scroll-container {
overflow: auto;
}
.shadow-top,
.shadow-right,
.shadow-bottom,
.shadow-left {
position: absolute;
border-radius: 6em;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.shadow-top,
.shadow-bottom {
right: 0;
left: 0;
height: 1em;
border-top-right-radius: 0;
border-top-left-radius: 0;
background-image: linear-gradient(rgba(#555, 0.1) 0%, rgba(#fff, 0) 100%);
2023-12-09 15:47:03 +01:00
}
.shadow-top {
top: 0;
}
.shadow-bottom {
bottom: 0;
transform: rotate(180deg);
}
.shadow-right,
.shadow-left {
top: 0;
bottom: 0;
width: 1em;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-image: linear-gradient(
90deg,
rgba(#555, 0.1) 0%,
rgba(#fff, 0) 100%
);
2023-12-09 15:47:03 +01:00
}
.shadow-right {
right: 0;
transform: rotate(180deg);
}
.shadow-left {
left: 0;
}
.is-active {
opacity: 1;
}
</style>