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.

203 lines
5.5 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="`${rootStyle};display: inline-block;`">
<!--强制设置高宽防止元素坍塌-->
<!--在使用 wd-sticky-box 某些情况下 wd-sticky__container 'positionabsolute' 需要相对于 wd-sticky-box-->
<view :class="`wd-sticky ${props.customClass}`" :style="stickyStyle">
<!--吸顶容器-->
<view class="wd-sticky__container" :style="containerStyle">
<!--监听元素尺寸变化-->
<wd-resize @resize="resizeHandler" custom-style="display: inline-block;">
<!--需要吸顶的内容-->
<slot />
</wd-resize>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-sticky',
options: {
addGlobalClass: true,
// virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { type Ref, computed, getCurrentInstance, inject, ref } from 'vue'
import { addUnit, getRect, objToStyle } from '../common/util'
interface Props {
customStyle?: string
customClass?: string
zIndex?: number
offsetTop?: number
}
const props = withDefaults(defineProps<Props>(), {
customStyle: '',
customClass: '',
zIndex: 1,
offsetTop: 0
})
const openBox = ref<boolean>(false)
const position = ref<string>('absolute')
const top = ref<number>(0)
const height = ref<number>(0)
const width = ref<number>(0)
const observerList = ref<UniApp.IntersectionObserver[]>([])
const state = ref<string>('')
const boxHeight: Ref<number> = inject('box-height', null) || ref(0)
// eslint-disable-next-line @typescript-eslint/ban-types
const observerForChild: Function | null = inject('observerForChild', null)
const { proxy } = getCurrentInstance() as any
const instance = getCurrentInstance() as any
const rootStyle = computed(() => {
const style: Record<string, string | number> = {
'z-index': props.zIndex,
height: addUnit(height.value),
width: addUnit(width.value)
}
if (!openBox.value) {
style['position'] = 'relative'
}
return `${objToStyle(style)};${props.customStyle}`
})
const stickyStyle = computed(() => {
const style: Record<string, string | number> = {
'z-index': props.zIndex,
height: addUnit(height.value),
width: addUnit(width.value)
}
if (!openBox.value) {
style['position'] = 'relative'
}
return `${objToStyle(style)};`
})
const containerStyle = computed(() => {
const style: Record<string, string | number> = {
position: position.value,
top: addUnit(top.value)
}
return objToStyle(style)
})
const innerOffsetTop = computed(() => {
let top: number = 0
// #ifdef H5
// H5端导航栏为普通元素需要将组件移动到导航栏的下边沿
// H5的导航栏高度为44px
top = 44
// #endif
return top + props.offsetTop
})
/**
* @description 清除无用的 viewport 观察者
*/
function clearObserver() {
while (observerList.value.length !== 0) {
observerList.value.pop()!.disconnect()
}
}
/**
* @description 创建新的 viewport 观察者
*/
function createObserver() {
const observer = uni.createIntersectionObserver(instance)
observerList.value.push(observer)
return observer
}
/**
* @description 监听到吸顶元素尺寸大小变化时,立即重新模拟吸顶
*/
function resizeHandler(detail) {
// 当吸顶内容处于absolute、fixed时为了防止父容器坍塌需要手动设置父容器高宽。
width.value = detail.width
height.value = detail.height
// // 如果和 wd-sticky-box 配套使用,吸顶逻辑交由 wd-sticky-box 进行处理
observerContentScroll()
if (!observerForChild) return
observerForChild(proxy)
}
/**
* @description 模拟吸顶逻辑
*/
function observerContentScroll() {
// 视图在 render tree 中未呈现,吸顶无任何意义。
if (height.value === 0 && width.value === 0) return
const offset = innerOffsetTop.value + height.value
clearObserver()
createObserver()
.relativeToViewport({
top: -offset // viewport上边界往下拉
})
.observe('.wd-sticky', scrollHandler)
getRect('.wd-sticky', false, proxy).then((res: any) => {
// 当 wd-sticky 位于 viewport 外部时不会触发 observe此时根据位置手动修复位置。
if (res.bottom <= offset) scrollHandler({ boundingClientRect: res })
})
}
/**
* @description 根据位置进行吸顶
*/
function scrollHandler({ boundingClientRect }) {
// sticky 高度大于或等于 wd-sticky-box使用 wd-sticky-box 无任何意义
if (observerForChild && height.value >= boxHeight.value) {
position.value = 'absolute'
top.value = 0
return
}
// boundingClientRect : 目标节点各个边在 viewport 中的坐标
if (boundingClientRect.top <= innerOffsetTop.value) {
state.value = 'sticky'
// 开始吸顶,固定到顶部
openBox.value = false
position.value = 'fixed'
top.value = innerOffsetTop.value
} else if (boundingClientRect.top > innerOffsetTop.value) {
state.value = 'normal'
// 完全展示,结束吸顶
openBox.value = false
position.value = 'absolute'
top.value = 0
}
}
/**
* 设置位置
* @param setOpenBox
* @param setPosition
* @param setTop
*/
function setPosition(setOpenBox: boolean, setPosition: string, setTop: number) {
openBox.value = setOpenBox
position.value = setPosition
top.value = setTop
}
defineExpose({
setPosition,
openBox: openBox,
position: position,
top: top,
height: height,
width: width,
state: state,
offsetTop: innerOffsetTop.value
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>