Files
veridian/app/composables/useAutoScroll.ts
T

82 lines
2.3 KiB
TypeScript

import { ref, watch, onUnmounted, type Ref } from 'vue';
export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: {
threshold?: number;
} = {}) {
const { threshold = 80 } = options;
const isUserScrollingUp = ref(false);
const shouldAutoScroll = ref(true);
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
const el = elementRef.value;
if (!el) return;
el.scrollTo({
top: el.scrollHeight,
behavior,
});
shouldAutoScroll.value = true;
};
const handleScroll = () => {
const el = elementRef.value;
if (!el) return;
const { scrollTop, scrollHeight, clientHeight } = el;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (distanceFromBottom <= threshold) {
if (isUserScrollingUp.value) {
isUserScrollingUp.value = false;
shouldAutoScroll.value = true;
}
} else {
isUserScrollingUp.value = true;
shouldAutoScroll.value = false;
}
};
let observer: MutationObserver | null = null;
let timeout: NodeJS.Timeout | null = null;
watch(elementRef, (newEl, oldEl) => {
if (oldEl) {
oldEl.removeEventListener('scroll', handleScroll);
observer?.disconnect();
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
}
if (newEl) {
newEl.addEventListener('scroll', handleScroll, { passive: true });
observer = new MutationObserver(() => {
// Only auto-scroll if user hasn't scrolled up and is near bottom
if (!isUserScrollingUp.value && shouldAutoScroll.value) {
scrollToBottom('instant');
}
});
observer.observe(newEl, {
childList: true,
subtree: true,
characterData: true,
});
}
});
onUnmounted(() => {
elementRef.value?.removeEventListener('scroll', handleScroll);
observer?.disconnect();
if (timeout) {
clearTimeout(timeout);
}
});
return {
scrollToBottom,
isUserScrollingUp, // expose for UI feedback (optional)
};
}