easy-pullrefresh
Version:
A lightweight JS library for pull to refresh
102 lines (86 loc) • 3.4 kB
text/typescript
interface PullRefreshOptions {
container: HTMLElement
onRefresh: () => Promise<void>
indicatorRender?: (status: 'idle' | 'pulling' | 'loading', distance: number) => string
threshold?: number
}
function initPullRefresh({ container, onRefresh, indicatorRender, threshold = 60 }: PullRefreshOptions) {
function dampedPull(deltaY: number): number {
if (deltaY <= threshold) return deltaY
return threshold + (deltaY - threshold) * 0.2
}
function updateStatus(status: 'idle' | 'pulling' | 'loading', distance: number) {
indicator.innerHTML = indicatorRender?.(status, distance) || ''
}
async function onTouchEnd() {
if (!pulling) return
pulling = false
indicator.style.transition = 'transform 0.3s'
container.style.transition = 'all 0.3s'
if (distance >= threshold) {
updateStatus('loading', 0)
container.style.transform = `translateY(${threshold}px)`
container.style.clipPath = `inset(0px 0px calc(${threshold}px))`
indicator.style.transform = `translateY(${threshold}px)`
await onRefresh()
}
container.style.transform = 'translateY(0)'
container.style.clipPath = `inset(0px 0px calc(0px))`
indicator.style.visibility = 'hidden'
updateStatus('idle', 0)
distance = 0
vibrateOnce = false
}
const { top } = container.getBoundingClientRect()
const parentNode = container.parentNode as HTMLElement
parentNode.style.position ||= 'relative'
let vibrateOnce = false
const indicator = document.createElement('div')
indicator.style.cssText = `
position: fixed;
top: ${top - 40}px;
left: 0;
width: 100%;
text-align: center;
pointer-events: none;
z-index: 10;
`
parentNode.prepend(indicator)
container.style.willChange = 'transform'
indicator.style.willChange = 'transform'
let pulling = false
let startY = 0
let distance = 0
container.addEventListener('touchstart', (e) => {
if (container.scrollTop === 0) {
startY = e.touches[0].clientY
pulling = true
}
})
container.addEventListener('touchmove', (e) => {
if (!pulling) return
const deltaY = e.touches[0].clientY - startY
if (deltaY <= 0) return
if (e.cancelable) {
e.preventDefault()
e.stopPropagation()
}
distance = dampedPull(deltaY)
container.style.transition = 'transform 0s'
indicator.style.transition = 'transform 0s'
indicator.style.visibility = 'visible'
requestAnimationFrame(() => {
container.style.transform = `translateY(${distance}px)`
indicator.style.transform = `translateY(${distance}px)`
container.style.clipPath = `inset(0px 0px calc(${distance}px))`
updateStatus('pulling', distance)
if (!vibrateOnce && distance >= threshold) {
navigator.vibrate(1000)
vibrateOnce = true
}
})
}, { passive: false })
container.addEventListener('touchend', onTouchEnd)
container.addEventListener('touchcancel', onTouchEnd)
}
export default initPullRefresh