You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
4.6 KiB
180 lines
4.6 KiB
import { nextTick } from 'vue'
|
|
import { throttle } from 'lodash-es'
|
|
import type { ComponentPublicInstance, ObjectDirective } from 'vue'
|
|
import {
|
|
getOffsetTopDistance,
|
|
getScrollContainer,
|
|
} from '@/utils/domUtils'
|
|
|
|
import { isFunction } from '@/utils/is'
|
|
|
|
export const SCOPE = 'ElInfiniteScroll'
|
|
export const CHECK_INTERVAL = 50
|
|
export const DEFAULT_DELAY = 200
|
|
export const DEFAULT_DISTANCE = 0
|
|
|
|
const attributes = {
|
|
delay: {
|
|
type: Number,
|
|
default: DEFAULT_DELAY,
|
|
},
|
|
distance: {
|
|
type: Number,
|
|
default: DEFAULT_DISTANCE,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
immediate: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
}
|
|
|
|
type Attrs = typeof attributes
|
|
type ScrollOptions = { [K in keyof Attrs]: Attrs[K]['default'] }
|
|
type InfiniteScrollCallback = () => void
|
|
type InfiniteScrollEl = HTMLElement & {
|
|
[SCOPE]: {
|
|
container: HTMLElement | Window
|
|
containerEl: HTMLElement
|
|
instance: ComponentPublicInstance
|
|
delay: number // export for test
|
|
lastScrollTop: number
|
|
cb: InfiniteScrollCallback
|
|
onScroll: () => void
|
|
observer?: MutationObserver
|
|
}
|
|
}
|
|
|
|
function getScrollOptions(el: HTMLElement, instance: ComponentPublicInstance): ScrollOptions {
|
|
return Object.entries(attributes).reduce((acm, [name, option]) => {
|
|
const { type, default: defaultValue } = option
|
|
const attrVal = el.getAttribute(`infinite-scroll-${name}`)!
|
|
let value = instance[attrVal] ?? attrVal ?? defaultValue
|
|
value = value === 'false' ? false : value
|
|
value = type(value)
|
|
acm[name] = Number.isNaN(value) ? defaultValue : value
|
|
return acm
|
|
}, {} as ScrollOptions)
|
|
}
|
|
|
|
function destroyObserver(el: InfiniteScrollEl) {
|
|
const { observer } = el[SCOPE]
|
|
|
|
if (observer) {
|
|
observer.disconnect()
|
|
delete el[SCOPE].observer
|
|
}
|
|
}
|
|
|
|
function handleScroll(el: InfiniteScrollEl, cb: InfiniteScrollCallback) {
|
|
const { container, containerEl, instance, observer, lastScrollTop }
|
|
= el[SCOPE]
|
|
const { disabled, distance } = getScrollOptions(el, instance)
|
|
const { clientHeight, scrollHeight, scrollTop } = containerEl
|
|
const delta = scrollTop - lastScrollTop
|
|
|
|
el[SCOPE].lastScrollTop = scrollTop
|
|
|
|
// trigger only if full check has done and not disabled and scroll down
|
|
if (observer || disabled || delta < 0)
|
|
return
|
|
|
|
let shouldTrigger = false
|
|
|
|
if (container === el) {
|
|
shouldTrigger = scrollHeight - (clientHeight + scrollTop) <= distance
|
|
}
|
|
else {
|
|
// get the scrollHeight since el might be visible overflow
|
|
const { clientTop, scrollHeight: height } = el
|
|
const offsetTop = getOffsetTopDistance(el, containerEl)
|
|
shouldTrigger
|
|
= scrollTop + clientHeight >= offsetTop + clientTop + height - distance
|
|
}
|
|
|
|
if (shouldTrigger)
|
|
cb.call(instance)
|
|
}
|
|
|
|
function checkFull(el: InfiniteScrollEl, cb: InfiniteScrollCallback) {
|
|
const { containerEl, instance } = el[SCOPE]
|
|
const { disabled } = getScrollOptions(el, instance)
|
|
|
|
if (disabled || containerEl.clientHeight === 0)
|
|
return
|
|
|
|
if (containerEl.scrollHeight <= containerEl.clientHeight)
|
|
cb.call(instance)
|
|
|
|
else
|
|
destroyObserver(el)
|
|
}
|
|
|
|
const InfiniteScroll: ObjectDirective<
|
|
InfiniteScrollEl,
|
|
InfiniteScrollCallback
|
|
> = {
|
|
async mounted(el, binding) {
|
|
const { instance, value: cb } = binding
|
|
|
|
if (!isFunction(cb))
|
|
throw new Error(`${SCOPE}'v-infinite-scroll' binding value must be a function`)
|
|
|
|
// ensure parentNode mounted
|
|
await nextTick()
|
|
|
|
const { delay, immediate } = getScrollOptions(el, instance as any)
|
|
const container = getScrollContainer(el, true)
|
|
const containerEl
|
|
= container === window
|
|
? document.documentElement
|
|
: (container as HTMLElement)
|
|
const onScroll = throttle(handleScroll.bind(null, el, cb), delay)
|
|
|
|
if (!container)
|
|
return
|
|
|
|
el[SCOPE] = {
|
|
instance: instance as any,
|
|
container,
|
|
containerEl,
|
|
delay,
|
|
cb,
|
|
onScroll,
|
|
lastScrollTop: containerEl.scrollTop,
|
|
}
|
|
|
|
if (immediate) {
|
|
const observer = new MutationObserver(
|
|
throttle(checkFull.bind(null, el, cb), CHECK_INTERVAL),
|
|
)
|
|
el[SCOPE].observer = observer
|
|
observer.observe(el, { childList: true, subtree: true })
|
|
checkFull(el, cb)
|
|
}
|
|
|
|
container.addEventListener('scroll', onScroll)
|
|
},
|
|
unmounted(el) {
|
|
const { container, onScroll } = el[SCOPE]
|
|
|
|
container?.removeEventListener('scroll', onScroll)
|
|
destroyObserver(el)
|
|
},
|
|
async updated(el) {
|
|
if (!el[SCOPE]) {
|
|
await nextTick()
|
|
}
|
|
else {
|
|
const { containerEl, cb, observer } = el[SCOPE]
|
|
if (containerEl.clientHeight && observer)
|
|
checkFull(el, cb)
|
|
}
|
|
},
|
|
}
|
|
|
|
export default InfiniteScroll
|