element-plus
Version:
A Component Library for Vue3.0
157 lines (131 loc) • 4.49 kB
text/typescript
import { nextTick } from 'vue'
import { isFunction } from '@vue/shared'
import throttle from 'lodash/throttle'
import { entries } from '@element-plus/utils/util'
import { getScrollContainer, getOffsetTopDistance } from '@element-plus/utils/dom'
import throwError from '@element-plus/utils/error'
import type { ObjectDirective, ComponentPublicInstance } from 'vue'
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
}
}
const getScrollOptions = (el: HTMLElement, instance: ComponentPublicInstance): ScrollOptions => {
return 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)
}
const destroyObserver = (el: InfiniteScrollEl) => {
const { observer } = el[SCOPE]
if (observer) {
observer.disconnect()
delete el[SCOPE].observer
}
}
const 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) 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)) {
throwError(SCOPE, '\'v-infinite-scroll\' binding value must be a function')
}
// ensure parentNode mounted
await nextTick()
const { delay, immediate } = getScrollOptions(el, instance)
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, 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)
},
}
export default InfiniteScroll