File size: 1,782 Bytes
7bf1507
feb88db
bf373ae
59de250
421ce25
cb5990d
421ce25
cb5990d
 
 
 
 
 
 
 
 
421ce25
cb5990d
421ce25
cb5990d
 
 
 
421ce25
cb5990d
 
421ce25
 
 
cb5990d
 
421ce25
7bf1507
421ce25
cb5990d
feb88db
 
cb5990d
421ce25
 
cb5990d
 
 
 
 
 
 
 
 
 
 
 
 
 
421ce25
cb5990d
070d5c7
cb5990d
070d5c7
421ce25
 
cb5990d
421ce25
cb5990d
 
98051f8
421ce25
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { navigating } from "$app/state";
import { tick } from "svelte";

export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => {
	let isDetached = false;
	const threshold = 50; // Distance from bottom to consider "attached"

	const isNearBottom = () => {
		const { scrollTop, scrollHeight, clientHeight } = node;
		// Use Math.abs for float precision safety, though distances are usually positive
		return Math.abs(scrollHeight - scrollTop - clientHeight) <= threshold;
	};

	const updateScrollPosition = () => {
		if (!isDetached) {
			node.scrollTo({ top: node.scrollHeight, behavior: "instant" });
		}
	};

	const onScroll = () => {
		// If the user is near the bottom, they are attached.
		// If they scroll up (away from bottom), they detach.
		if (isNearBottom()) {
			isDetached = false;
		} else {
			isDetached = true;
		}
	};

	const update = async (_options: { force?: boolean } = {}) => {
		const { force = false } = _options;

		if (!force && isDetached && !navigating.to) return;

		// Wait for DOM updates (e.g. new message rendered)
		await tick();

		node.scrollTo({ top: node.scrollHeight, behavior: "instant" });
	};

	// Observe content size changes (e.g. streaming responses, images loading)
	// This ensures we stay at the bottom even if the container size doesn't change
	// but the content grows.
	const observer = new ResizeObserver(() => {
		updateScrollPosition();
	});

	if (node.firstElementChild) {
		observer.observe(node.firstElementChild);
	} else {
		observer.observe(node);
	}

	node.addEventListener("scroll", onScroll);

	// Check initial state
	if (dependency) {
		update({ force: true });
	}

	return {
		update,
		destroy: () => {
			node.removeEventListener("scroll", onScroll);
			observer.disconnect();
		},
	};
};