import { throttle } from '@opentiny/utils'
import { getScrollContainer } from '@opentiny/utils'
const CONTEXT_KEY = '@@infinitescrollContext'
const OBSERVER_CHECK_INTERVAL = 50
const ATTR_DEFAULT_DELAY = 200
const ATTR_DEFAULT_DISTANCE = 0
const attrs = {
delay: { type: Number, default: ATTR_DEFAULT_DELAY },
disabled: { type: Boolean, default: false },
distance: { type: Number, default: ATTR_DEFAULT_DISTANCE },
immediate: { type: Boolean, default: true }
}
const isNull = (val) => val === null
const parseAttrValue = (attrVal, type, defaultVal) => {
if (isNull(attrVal)) return defaultVal
if (type === Boolean) {
return attrVal !== 'false'
} else if (type === Number) {
return Number(attrVal)
}
}
const computeScrollOptions = (el, instance) =>
Object.entries(attrs).reduce((accumulator, [name, option]) => {
const { type, default: defaultValue } = option
const attrKey = `infinite-scroll-${name}`
const $attrValue =
instance?.$el && typeof instance.$el.getAttribute === 'function' ? instance.$el.getAttribute(attrKey) : null
const attrValue = el.getAttribute(attrKey)
let value
if ((isNull(attrValue) && isNull($attrValue)) || !isNull(attrValue)) {
value = parseAttrValue(attrValue, type, defaultValue)
}
if (isNull(attrValue) && !isNull($attrValue)) {
value = parseAttrValue($attrValue, type, defaultValue)
}
accumulator[name] = Number.isNaN(value) ? defaultValue : value
return accumulator
}, {})
const stopObserver = (el) => {
const { observer } = el[CONTEXT_KEY]
if (observer) {
observer.disconnect()
delete el[CONTEXT_KEY].observer
}
}
const accumOffsetTop = (el) => {
let totalOffset = 0
let parent = el
while (parent) {
totalOffset += parent.offsetTop
parent = parent.offsetParent
}
return totalOffset
}
const distanceOffsetTop = (el, containerEl) => Math.abs(accumOffsetTop(el) - accumOffsetTop(containerEl))
const scroller = (el, cb) => {
const { container, containerEl, instance, observer, lastScrollTop } = el[CONTEXT_KEY]
const { disabled, distance } = computeScrollOptions(el, instance)
const { clientHeight, scrollHeight, scrollTop } = containerEl
const deltaTop = scrollTop - lastScrollTop
el[CONTEXT_KEY].lastScrollTop = scrollTop
if (observer || disabled || deltaTop < 0) return
let isTrigger = false
if (container === el) {
isTrigger = scrollHeight - (clientHeight + scrollTop) <= distance
} else {
const { clientTop, scrollHeight: height } = el
const offsetTop = distanceOffsetTop(el, containerEl)
isTrigger = scrollTop + clientHeight >= offsetTop + clientTop + height - distance
}
if (isTrigger) {
cb.call(instance)
}
}
function observerChecker(el, cb) {
const { containerEl, instance } = el[CONTEXT_KEY]
const { disabled } = computeScrollOptions(el, instance)
if (disabled || containerEl.clientHeight === 0) return
if (containerEl.scrollHeight <= containerEl.clientHeight) {
cb.call(instance)
} else {
stopObserver(el)
}
}
const bind = (el, binding, vnode) => {
const instance = binding.instance || vnode.context
const { value: cb } = binding
if (typeof cb !== 'function') {
throw new TypeError('[TINY Error][InfiniteScroll] "v-infinite-scroll" binding value must be a function')
}
instance.$nextTick(() => {
const { delay, immediate } = computeScrollOptions(el, instance)
const container = getScrollContainer(el, true)
const containerEl = container === window ? document.documentElement : container
const onScroll = throttle(delay, scroller.bind(null, el, cb))
if (!container) return
el[CONTEXT_KEY] = { instance, container, containerEl, delay, cb, onScroll, lastScrollTop: containerEl.scrollTop }
if (immediate) {
const observer = new MutationObserver(throttle(OBSERVER_CHECK_INTERVAL, observerChecker.bind(null, el, cb)))
el[CONTEXT_KEY].observer = observer
observer.observe(el, { childList: true, subtree: true })
observerChecker(el, cb)
}
container.addEventListener('scroll', onScroll)
})
}
const update = (el, binding, vnode) => {
if (!el[CONTEXT_KEY]) {
const instance = binding.instance || vnode.context
return instance.$nextTick()
} else {
const { containerEl, cb, observer } = el[CONTEXT_KEY]
if (containerEl.clientHeight && observer) {
observerChecker(el, cb)
}
}
}
const unbind = (el) => {
const { container, onScroll } = el[CONTEXT_KEY]
if (container) container.removeEventListener('scroll', onScroll)
stopObserver(el)
}
export default {
bind,
update,
unbind,
beforeMount: bind,
updated: update,
unmounted: unbind
}