forked from nikita/muzika-gromche
152 lines
3.2 KiB
Vue
152 lines
3.2 KiB
Vue
<script lang="ts">
|
|
import type { Handler } from 'mitt'
|
|
import { useRafFn } from '@vueuse/core'
|
|
import mitt from 'mitt'
|
|
// eslint-disable-next-line import/no-duplicates
|
|
import { onBeforeUnmount, onMounted } from 'vue'
|
|
|
|
interface ScrollSyncEvent {
|
|
scrollTop: number
|
|
scrollHeight: number
|
|
clientHeight: number
|
|
scrollLeft: number
|
|
scrollWidth: number
|
|
clientWidth: number
|
|
barHeight: number
|
|
barWidth: number
|
|
emitter: string
|
|
group: string
|
|
}
|
|
|
|
type Events = {
|
|
'scroll-sync': ScrollSyncEvent
|
|
}
|
|
|
|
const emitter = mitt<Events>()
|
|
|
|
function useEvent<Key extends keyof Events>(
|
|
type: Key,
|
|
handler: Handler<Events[Key]>,
|
|
): void {
|
|
const { on, off } = emitter
|
|
|
|
onMounted(() => {
|
|
on(type, handler)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
off(type, handler)
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
// eslint-disable-next-line import/first, import/no-duplicates
|
|
import { useId, useTemplateRef } from 'vue'
|
|
|
|
const {
|
|
proportional,
|
|
vertical,
|
|
horizontal,
|
|
group,
|
|
} = defineProps<{
|
|
proportional?: boolean
|
|
vertical?: boolean
|
|
horizontal?: boolean
|
|
group: string
|
|
}>()
|
|
|
|
const uuid = useId()
|
|
const nodeRef = useTemplateRef('scroll-sync-container')
|
|
|
|
defineExpose({
|
|
scrollTo: (options: ScrollToOptions) => {
|
|
nodeRef.value?.scrollTo(options)
|
|
},
|
|
})
|
|
|
|
function handleScroll(event: Event) {
|
|
useRafFn(() => {
|
|
const {
|
|
scrollTop,
|
|
scrollHeight,
|
|
clientHeight,
|
|
scrollLeft,
|
|
scrollWidth,
|
|
clientWidth,
|
|
offsetHeight,
|
|
offsetWidth,
|
|
} = event.target as HTMLElement
|
|
|
|
emitter.emit('scroll-sync', {
|
|
scrollTop,
|
|
scrollHeight,
|
|
clientHeight,
|
|
scrollLeft,
|
|
scrollWidth,
|
|
clientWidth,
|
|
barHeight: offsetHeight - clientHeight,
|
|
barWidth: offsetWidth - clientWidth,
|
|
emitter: uuid,
|
|
group,
|
|
})
|
|
}, { once: true })
|
|
}
|
|
|
|
useEvent('scroll-sync', (event: ScrollSyncEvent) => {
|
|
const node = nodeRef.value
|
|
|
|
if (event.group !== group || event.emitter === uuid || node === null) {
|
|
return
|
|
}
|
|
|
|
const {
|
|
scrollTop,
|
|
scrollHeight,
|
|
clientHeight,
|
|
scrollLeft,
|
|
scrollWidth,
|
|
clientWidth,
|
|
barHeight,
|
|
barWidth,
|
|
} = event
|
|
|
|
// from https://github.com/okonet/react-scroll-sync
|
|
const scrollTopOffset = scrollHeight - clientHeight
|
|
const scrollLeftOffset = scrollWidth - clientWidth
|
|
|
|
/* Calculate the actual pane height */
|
|
const paneHeight = node.scrollHeight - clientHeight
|
|
const paneWidth = node.scrollWidth - clientWidth
|
|
|
|
/* Adjust the scrollTop position of it accordingly */
|
|
node.removeEventListener('scroll', handleScroll)
|
|
if (vertical && scrollTopOffset > barHeight) {
|
|
node.scrollTop = proportional ? (paneHeight * scrollTop) / scrollTopOffset : scrollTop
|
|
}
|
|
if (horizontal && scrollLeftOffset > barWidth) {
|
|
node.scrollLeft = proportional ? (paneWidth * scrollLeft) / scrollLeftOffset : scrollLeft
|
|
}
|
|
useRafFn(() => {
|
|
node.addEventListener('scroll', handleScroll)
|
|
}, { once: true })
|
|
})
|
|
|
|
onMounted(() => {
|
|
const node = nodeRef.value
|
|
node!.addEventListener('scroll', handleScroll)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="scroll-sync-container" class="scroll-sync-container">
|
|
<slot />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.scroll-sync-container {
|
|
overflow: auto;
|
|
}
|
|
</style>
|