| | import { AUTO_SCROLL_AT_BOTTOM_THRESHOLD, AUTO_SCROLL_INTERVAL } from '$lib/constants/auto-scroll';
|
| |
|
| | export interface AutoScrollOptions {
|
| |
|
| | disabled?: boolean;
|
| | }
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | export class AutoScrollController {
|
| | private _autoScrollEnabled = $state(true);
|
| | private _userScrolledUp = $state(false);
|
| | private _lastScrollTop = $state(0);
|
| | private _scrollInterval: ReturnType<typeof setInterval> | undefined;
|
| | private _scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
| | private _container: HTMLElement | undefined;
|
| | private _disabled: boolean;
|
| |
|
| | constructor(options: AutoScrollOptions = {}) {
|
| | this._disabled = options.disabled ?? false;
|
| | }
|
| |
|
| | get autoScrollEnabled(): boolean {
|
| | return this._autoScrollEnabled;
|
| | }
|
| |
|
| | get userScrolledUp(): boolean {
|
| | return this._userScrolledUp;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | setContainer(container: HTMLElement | undefined): void {
|
| | this._container = container;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | setDisabled(disabled: boolean): void {
|
| | this._disabled = disabled;
|
| | if (disabled) {
|
| | this._autoScrollEnabled = false;
|
| | this.stopInterval();
|
| | }
|
| | }
|
| |
|
| | |
| | |
| |
|
| | handleScroll(): void {
|
| | if (this._disabled || !this._container) return;
|
| |
|
| | const { scrollTop, scrollHeight, clientHeight } = this._container;
|
| | const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
| | const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
|
| |
|
| | if (scrollTop < this._lastScrollTop && !isAtBottom) {
|
| | this._userScrolledUp = true;
|
| | this._autoScrollEnabled = false;
|
| | } else if (isAtBottom && this._userScrolledUp) {
|
| | this._userScrolledUp = false;
|
| | this._autoScrollEnabled = true;
|
| | }
|
| |
|
| | if (this._scrollTimeout) {
|
| | clearTimeout(this._scrollTimeout);
|
| | }
|
| |
|
| | this._scrollTimeout = setTimeout(() => {
|
| | if (isAtBottom) {
|
| | this._userScrolledUp = false;
|
| | this._autoScrollEnabled = true;
|
| | }
|
| | }, AUTO_SCROLL_INTERVAL);
|
| |
|
| | this._lastScrollTop = scrollTop;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | scrollToBottom(behavior: ScrollBehavior = 'smooth'): void {
|
| | if (this._disabled || !this._container) return;
|
| |
|
| | this._container.scrollTo({
|
| | top: this._container.scrollHeight,
|
| | behavior
|
| | });
|
| | }
|
| |
|
| | |
| | |
| |
|
| | enable(): void {
|
| | if (this._disabled) return;
|
| | this._userScrolledUp = false;
|
| | this._autoScrollEnabled = true;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | startInterval(): void {
|
| | if (this._disabled || this._scrollInterval) return;
|
| |
|
| | this._scrollInterval = setInterval(() => {
|
| | this.scrollToBottom();
|
| | }, AUTO_SCROLL_INTERVAL);
|
| | }
|
| |
|
| | |
| | |
| |
|
| | stopInterval(): void {
|
| | if (this._scrollInterval) {
|
| | clearInterval(this._scrollInterval);
|
| | this._scrollInterval = undefined;
|
| | }
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | updateInterval(isStreaming: boolean): void {
|
| | if (this._disabled) {
|
| | this.stopInterval();
|
| | return;
|
| | }
|
| |
|
| | if (isStreaming && this._autoScrollEnabled) {
|
| | if (!this._scrollInterval) {
|
| | this.startInterval();
|
| | }
|
| | } else {
|
| | this.stopInterval();
|
| | }
|
| | }
|
| |
|
| | |
| | |
| |
|
| | destroy(): void {
|
| | this.stopInterval();
|
| | if (this._scrollTimeout) {
|
| | clearTimeout(this._scrollTimeout);
|
| | this._scrollTimeout = undefined;
|
| | }
|
| | }
|
| | }
|
| |
|
| | |
| | |
| |
|
| | export function createAutoScrollController(options: AutoScrollOptions = {}): AutoScrollController {
|
| | return new AutoScrollController(options);
|
| | }
|
| |
|