Files
chad/new-client/src/widgets/chat/composables/use-chat-scroll.ts
2026-06-01 05:07:42 +06:00

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,
}
}