| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- import { createEffect, on, onCleanup } from "solid-js"
- import { createStore } from "solid-js/store"
- import { createResizeObserver } from "@solid-primitives/resize-observer"
- export interface AutoScrollOptions {
- working: () => boolean
- onUserInteracted?: () => void
- }
- export function createAutoScroll(options: AutoScrollOptions) {
- let scroll: HTMLElement | undefined
- let settling = false
- let settleTimer: ReturnType<typeof setTimeout> | undefined
- let down = false
- let cleanup: (() => void) | undefined
- const [store, setStore] = createStore({
- contentRef: undefined as HTMLElement | undefined,
- userScrolled: false,
- })
- const active = () => options.working() || settling
- const distanceFromBottom = () => {
- const el = scroll
- if (!el) return 0
- return el.scrollHeight - el.clientHeight - el.scrollTop
- }
- const scrollToBottomNow = (behavior: ScrollBehavior) => {
- const el = scroll
- if (!el) return
- el.scrollTo({ top: el.scrollHeight, behavior })
- }
- const scrollToBottom = (force: boolean) => {
- if (!force && !active()) return
- if (!scroll) return
- if (!force && store.userScrolled) return
- if (force && store.userScrolled) setStore("userScrolled", false)
- const distance = distanceFromBottom()
- if (distance < 2) return
- const behavior: ScrollBehavior = force || distance > 96 ? "auto" : "smooth"
- scrollToBottomNow(behavior)
- }
- const stop = () => {
- if (!active()) return
- if (store.userScrolled) return
- setStore("userScrolled", true)
- options.onUserInteracted?.()
- }
- const handleWheel = (e: WheelEvent) => {
- if (e.deltaY >= 0) return
- stop()
- }
- const handlePointerUp = () => {
- down = false
- window.removeEventListener("pointerup", handlePointerUp)
- }
- const handlePointerDown = () => {
- if (down) return
- down = true
- window.addEventListener("pointerup", handlePointerUp)
- }
- const handleTouchEnd = () => {
- down = false
- window.removeEventListener("touchend", handleTouchEnd)
- }
- const handleTouchStart = () => {
- if (down) return
- down = true
- window.addEventListener("touchend", handleTouchEnd)
- }
- const handleScroll = () => {
- if (!active()) return
- if (!scroll) return
- if (distanceFromBottom() < 10) {
- if (store.userScrolled) setStore("userScrolled", false)
- return
- }
- if (down) stop()
- }
- const handleInteraction = () => {
- stop()
- }
- createResizeObserver(
- () => store.contentRef,
- () => {
- if (!active()) return
- if (store.userScrolled) return
- scrollToBottom(false)
- },
- )
- createEffect(
- on(options.working, (working) => {
- settling = false
- if (settleTimer) clearTimeout(settleTimer)
- settleTimer = undefined
- setStore("userScrolled", false)
- if (working) {
- scrollToBottom(true)
- return
- }
- settling = true
- settleTimer = setTimeout(() => {
- settling = false
- }, 300)
- }),
- )
- onCleanup(() => {
- if (settleTimer) clearTimeout(settleTimer)
- if (cleanup) cleanup()
- })
- return {
- scrollRef: (el: HTMLElement | undefined) => {
- if (cleanup) {
- cleanup()
- cleanup = undefined
- }
- scroll = el
- down = false
- if (!el) return
- el.style.overflowAnchor = "none"
- el.addEventListener("wheel", handleWheel, { passive: true })
- el.addEventListener("pointerdown", handlePointerDown)
- el.addEventListener("touchstart", handleTouchStart, { passive: true })
- cleanup = () => {
- el.removeEventListener("wheel", handleWheel)
- el.removeEventListener("pointerdown", handlePointerDown)
- el.removeEventListener("touchstart", handleTouchStart)
- window.removeEventListener("pointerup", handlePointerUp)
- window.removeEventListener("touchend", handleTouchEnd)
- }
- },
- contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el),
- handleScroll,
- handleInteraction,
- scrollToBottom: () => scrollToBottom(false),
- forceScrollToBottom: () => scrollToBottom(true),
- userScrolled: () => store.userScrolled,
- }
- }
|