You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
4.1 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view style="position: relative">
<view :class="`wd-sticky-box ${props.customClass}`">
<wd-resize @resize="resizeHandler">
<slot />
</wd-resize>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-sticky-box',
options: {
addGlobalClass: true,
// virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { getCurrentInstance, onBeforeMount, provide, ref } from 'vue'
import { getRect } from '../common/util'
interface Props {
customClass?: string
}
const props = withDefaults(defineProps<Props>(), {
customClass: ''
})
const observerMap = ref<Map<any, any>>(new Map())
const height = ref<number>(0)
const width = ref<number>(0)
const stickyList: any[] = [] // 子元素sticky列表
const { proxy } = getCurrentInstance() as any
const instance = getCurrentInstance() as any
provide('box-height', height)
provide('box-width', width)
provide('observerForChild', observerForChild)
onBeforeMount(() => {
observerMap.value = new Map()
})
/**
* @description wd-sticky-box 尺寸发生变化时重新监听所有的viewport
*/
function resizeHandler(detail) {
// 相对的容器大小改变后,同步设置 wd-sticky-box 的大小
width.value = detail.width
height.value = detail.height
// wd-sticky-box 大小变化时,重新监听所有吸顶元素
const temp = observerMap.value
observerMap.value = new Map()
for (const [uid] of temp) {
const child = stickyList.find((sticky) => {
return sticky.$.uid === uid
})
observerForChild(child)
}
temp.forEach((observer) => {
observer.disconnect()
})
temp.clear()
}
/**
* @description 删除 wd-sticky 废弃的监听器
* @param child
*/
function deleteObserver(child) {
const observer = observerMap.value.get(child.$.uid)
if (!observer) return
observer.disconnect()
observerMap.value.delete(child.$.uid)
}
/**
* @description 为 wd-sticky 创建监听器
* @param child
*/
function createObserver(child) {
const observer = uni.createIntersectionObserver(instance)
observerMap.value.set(child.$.uid, observer)
return observer
}
/**
* @description 为单个 wd-sticky 监听 viewport
* @param child sticky
*/
function observerForChild(child) {
const hasChild = stickyList.find((sticky) => {
return sticky.$.uid === child.$.uid
})
if (!hasChild) {
stickyList.push(child)
}
deleteObserver(child)
const observer = createObserver(child)
const exposed = child.$.exposed
const offset = exposed.height.value + exposed.offsetTop
// 如果 wd-sticky 比 wd-sticky-box还大"相对吸顶"无任何意义,此时强制吸顶元素回归其占位符
if (height.value <= exposed.height.value) {
exposed.setPosition(false, 'absolute', 0)
}
observer.relativeToViewport({ top: -offset }).observe('.wd-sticky-box', (result) => {
scrollHandler(exposed, result)
})
getRect('.wd-sticky-box', false, proxy).then((res: any) => {
// 当 wd-sticky-box 位于 viewport 外部时不会触发 observe此时根据位置手动修复位置。
if (res.bottom <= offset) scrollHandler(exposed, { boundingClientRect: res })
})
}
/**
* @description 为子节点监听 viewport处理子节点的相对吸顶逻辑
* @param {Object} exposed wd-sticky实例暴露出的事件
* @param {Object} boundingClientRect 目标节点各个边在viewport中的坐标
*/
function scrollHandler(exposed, { boundingClientRect }) {
const offset = exposed.height.value + exposed.offsetTop
if (boundingClientRect.bottom <= offset) {
// 父元素即将被吸顶元素遮盖,将吸顶元素固定到父元素底部
exposed.setPosition(true, 'absolute', boundingClientRect.height - exposed.height.value)
} else if (boundingClientRect.top <= offset && boundingClientRect.bottom > offset) {
// wd-sticky 已经完全呈现了 viewport 中了,
// 此时没有必要再相对 wd-sticky-box 吸顶了
if (exposed.state.value === 'normal') return
// 顶元素开始遮盖不住父元素了,将吸顶元素恢复到吸顶模式
exposed.setPosition(false, 'fixed', exposed.offsetTop)
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>