111 lines
3.2 KiB
TypeScript
111 lines
3.2 KiB
TypeScript
import type { MaybeRefOrGetter, TemplateRef } from 'vue'
|
|
import { useEventListener, useMutationObserver } from '@vueuse/core'
|
|
import { computed, reactive, toValue, watch } from 'vue'
|
|
|
|
interface ChatScrollOptions {
|
|
startOffset?: number
|
|
endOffset?: number
|
|
}
|
|
|
|
export function useChatScroll(target: TemplateRef<HTMLElement>, options?: MaybeRefOrGetter<ChatScrollOptions>) {
|
|
const el = computed(() => toValue(target))
|
|
|
|
const arrivedState = reactive({
|
|
start: true,
|
|
end: false,
|
|
})
|
|
|
|
let lastScrollHeight = 0
|
|
let lastScrollTop = 0
|
|
|
|
watch(el, (el) => {
|
|
if (!el)
|
|
return
|
|
|
|
scrollToStart(true)
|
|
getArrivedState()
|
|
}, { flush: 'post' })
|
|
|
|
useMutationObserver(el, (mutations) => {
|
|
if (arrivedState.start) {
|
|
scrollToStart(true)
|
|
}
|
|
else {
|
|
// const prepended = mutations.some(mutation => Array.from(mutation.addedNodes.values()).some(node => !!node.nextEle))
|
|
// console.log('prepended', prepended)
|
|
// for (const mutation of mutations) {
|
|
// for (const node of mutation.addedNodes) {
|
|
// console.log('added node', node, !!node.nextSibling)
|
|
// if (node.nextSibling) {
|
|
// // добавлено сверху/в середину
|
|
// // el.value!.scrollTop += (node as HTMLElement).offsetHeight
|
|
// }
|
|
// }
|
|
//
|
|
// // for (const node of mutation.removedNodes) {
|
|
// // // previousSibling/nextSibling здесь — это соседи на момент удаления
|
|
// // if (mutation.previousSibling || mutation.nextSibling) {
|
|
// // // удалено сверху/в середину — уменьшаем scrollTop
|
|
// // el.value!.scrollTop -= (node as HTMLElement).offsetHeight
|
|
// // }
|
|
// // }
|
|
// }
|
|
|
|
const heightDiff = el.value!.scrollHeight - lastScrollHeight
|
|
// const scrollDiff = el.value!.scrollTop - lastScrollTop
|
|
//
|
|
// console.log('height', 'old', lastScrollHeight, 'new', el.value!.scrollHeight, 'diff', heightDiff)
|
|
// console.log('scroll', 'old', lastScrollTop, 'new', el.value!.scrollTop, 'diff', scrollDiff)
|
|
//
|
|
el.value!.scrollTop += heightDiff
|
|
}
|
|
|
|
getArrivedState()
|
|
}, {
|
|
childList: true,
|
|
subtree: true,
|
|
})
|
|
|
|
useEventListener(el, 'scroll', () => {
|
|
getArrivedState()
|
|
lastScrollTop = el.value!.scrollTop
|
|
}, { passive: true })
|
|
|
|
useEventListener(el, 'scrollend', () => {
|
|
getArrivedState()
|
|
}, { passive: true })
|
|
|
|
function getArrivedState() {
|
|
if (!el.value) {
|
|
arrivedState.start = true
|
|
arrivedState.end = false
|
|
return
|
|
}
|
|
|
|
const { startOffset = 0, endOffset = 0 } = toValue(options) ?? {}
|
|
|
|
const offsetHeight = el.value.offsetHeight
|
|
const scrollHeight = el.value.scrollHeight
|
|
const scrollTop = Math.abs(el.value.scrollTop)
|
|
|
|
arrivedState.start = scrollTop + offsetHeight >= scrollHeight - startOffset
|
|
arrivedState.end = scrollTop <= endOffset
|
|
|
|
lastScrollHeight = el.value!.scrollHeight
|
|
}
|
|
|
|
function scrollToStart(instant = false) {
|
|
if (!el.value)
|
|
return
|
|
|
|
const offset = Math.ceil(el.value.scrollHeight - el.value.offsetHeight)
|
|
|
|
el.value.scrollTo({ top: offset, behavior: instant ? 'instant' : 'smooth' })
|
|
}
|
|
|
|
return {
|
|
arrivedState,
|
|
scrollToStart,
|
|
}
|
|
}
|