import { createEffect, createRenderEffect, createSignal, onCleanup, Show, untrack, type Component, type Signal, } from "solid-js"; import { css } from "solid-styled"; import { Refresh as RefreshIcon } from "@suid/icons-material"; import { CircularProgress } from "@suid/material"; import { createEventListener, makeEventListener, } from "@solid-primitives/event-listener"; import { createViewportObserver, createVisibilityObserver, } from "@solid-primitives/intersection-observer"; const PullDownToRefresh: Component<{ loading?: boolean; linkedElement?: HTMLElement; onRefresh?: () => void; }> = (props) => { let rootElement: HTMLDivElement; const [pullDown, setPullDown] = createSignal(0); const pullDownDistance = () => { if (props.loading) { return 140; } return Math.max(Math.min(160, pullDown()), 0); }; const obvx = createVisibilityObserver({ threshold: 0.0001, }); const rootVisible = obvx(() => rootElement); let released = true; let v = 0; let lts = -1; let ds = 0; let holding = false; const M = 1; const K = -10; const D = -10; const updatePullDown = (ts: number) => { released = false; try { const x = untrack(pullDown); const dt = lts !== -1 ? ts - lts : 1 / 60; const fs = (ds / Math.pow(dt, 2)) * M; const fh = (x + ds) * K + D * v; const f = fs + fh; const a = f / M; v += a * dt; if (holding && v < 0) { v = 0 } setPullDown(x + v * dt); if (Math.abs(x) > 1 || Math.abs(v) > 1) { requestAnimationFrame(updatePullDown); } else { v = 0; lts = -1; } if (!holding && untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) { setTimeout(props.onRefresh, 0) } } finally { ds = 0; released = true; } }; const handleLinkedWheel = (event: WheelEvent) => { const scrollTop = (event.target as HTMLElement).scrollTop; if (scrollTop >= 0 && scrollTop < 1) { ds = -(event.deltaY / window.devicePixelRatio); if (released) { released = false; requestAnimationFrame(updatePullDown); } } }; createEffect((cleanup?: () => void) => { if (!rootVisible()) { return; } cleanup?.(); const element = props.linkedElement; if (!element) return; return makeEventListener(element, "wheel", handleLinkedWheel); }); let lastTouchId: number | undefined = undefined; let lastTouchScreenY = 0; const handleTouch = (event: TouchEvent) => { if (event.targetTouches.length > 1) { lastTouchId = 0; lastTouchScreenY; return; } const item = event.targetTouches.item(0)!; if (lastTouchId && item.identifier !== lastTouchId) { lastTouchId = undefined; lastTouchScreenY = 0; return; } holding = true; if (lastTouchScreenY !== 0) { ds = item.screenY - lastTouchScreenY; } lastTouchScreenY = item.screenY; if (released) { released = false; requestAnimationFrame(updatePullDown); } }; const handleTouchEnd = () => { lastTouchId = undefined; lastTouchScreenY = 0; holding = false; if (untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) { setTimeout(props.onRefresh, 0) } }; createEffect((cleanup?: () => void) => { if (!rootVisible()) { return; } cleanup?.(); const element = props.linkedElement; if (!element) return; const cleanup0 = makeEventListener(element, "touchmove", handleTouch); const cleanup1 = makeEventListener(element, "touchend", handleTouchEnd); return () => (cleanup0(), cleanup1()); }); css` .pull-down { width: 100%; display: flex; justify-content: center; margin-top: -2rem; height: calc(1px + 2rem); } .indicator { display: inline-flex; justify-content: center; align-items: center; box-shadow: ${props.loading ? "var(--tutu-shadow-e12)" : "var(--tutu-shadow-e1)"}; border-radius: 50%; aspect-ratio: 1/1; width: 2rem; color: var(--tutu-color-primary); transform: translateY(${`${pullDownDistance() - 2}px`}); will-change: transform; z-index: var(--tutu-zidx-nav); background-color: var(--tutu-color-surface); > :global(.refresh-icon) { transform: rotate( ${`${((pullDownDistance() / 160) * 180).toString()}deg`} ); will-change: transform; } > :global(.refresh-indicator) { width: 1.5rem; height: 1.5rem; aspect-ratio: 1/1; } } `; return (