wd-sticky-box.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. <template>
  2. <view style="position: relative">
  3. <view :class="`wd-sticky-box ${props.customClass}`" :style="customStyle" :id="styckyBoxId">
  4. <wd-resize @resize="handleResize">
  5. <slot />
  6. </wd-resize>
  7. </view>
  8. </view>
  9. </template>
  10. <script lang="ts">
  11. export default {
  12. name: 'wd-sticky-box',
  13. options: {
  14. addGlobalClass: true,
  15. // virtualHost: true,
  16. styleIsolation: 'shared'
  17. }
  18. }
  19. </script>
  20. <script lang="ts" setup>
  21. import wdResize from '../wd-resize/wd-resize.vue'
  22. import { getCurrentInstance, onBeforeMount, reactive, ref } from 'vue'
  23. import { getRect, uuid } from '../common/util'
  24. import { baseProps } from '../common/props'
  25. import { STICKY_BOX_KEY } from './types'
  26. import { useChildren } from '../composables/useChildren'
  27. const props = defineProps(baseProps)
  28. const styckyBoxId = ref<string>(`wd-sticky-box${uuid()}`)
  29. const observerMap = ref<Map<any, any>>(new Map())
  30. const boxStyle = reactive({
  31. height: 0,
  32. width: 0
  33. })
  34. const { proxy } = getCurrentInstance() as any
  35. const { children: stickyList, linkChildren } = useChildren(STICKY_BOX_KEY)
  36. linkChildren({
  37. boxStyle: boxStyle,
  38. observerForChild
  39. })
  40. onBeforeMount(() => {
  41. observerMap.value = new Map()
  42. })
  43. /**
  44. * 容器大小变化后重新监听sticky组件与box组件的交叉状态
  45. * @param detail
  46. */
  47. function handleResize(detail: any) {
  48. // 相对的容器大小改变后,同步设置 wd-sticky-box 的大小
  49. boxStyle.width = detail.width
  50. boxStyle.height = detail.height
  51. // wd-sticky-box 大小变化时,重新监听所有吸顶元素
  52. const temp = observerMap.value
  53. observerMap.value = new Map()
  54. for (const [uid] of temp) {
  55. const child = stickyList.find((sticky) => {
  56. return sticky.$.uid === uid
  57. })
  58. observerForChild(child)
  59. }
  60. temp.forEach((observer) => {
  61. observer.disconnect()
  62. })
  63. temp.clear()
  64. }
  65. /**
  66. * 删除对指定sticky的监听
  67. * @param child 指定的子组件
  68. */
  69. function deleteObserver(child: any) {
  70. const observer = observerMap.value.get(child.$.uid)
  71. if (!observer) return
  72. observer.disconnect()
  73. observerMap.value.delete(child.$.uid)
  74. }
  75. /**
  76. * 针对指定sticky添加监听
  77. * @param child 指定的子组件
  78. */
  79. function createObserver(child: any) {
  80. const observer = uni.createIntersectionObserver(proxy, { thresholds: [0, 0.5] })
  81. observerMap.value.set(child.$.uid, observer)
  82. return observer
  83. }
  84. /**
  85. * 监听子组件
  86. * @param child 子组件
  87. */
  88. function observerForChild(child: any) {
  89. deleteObserver(child)
  90. const observer = createObserver(child)
  91. const exposed = child.$.exposed
  92. let offset = exposed.stickyState.height + exposed.offsetTop
  93. // #ifdef H5
  94. // H5端,导航栏为普通元素,需要将组件移动到导航栏的下边沿
  95. // H5的导航栏高度为44px
  96. offset = offset + 44
  97. // #endif
  98. if (boxStyle.height <= exposed.stickyState.height) {
  99. exposed.setPosition(false, 'absolute', 0)
  100. }
  101. observer.relativeToViewport({ top: -offset }).observe(`#${styckyBoxId.value}`, (result) => {
  102. handleRelativeTo(exposed, result)
  103. })
  104. // 当子组件默认处于边界外且永远不会进入边界内时,需要手动调用一次
  105. getRect(`#${styckyBoxId.value}`, false, proxy)
  106. .then((res) => {
  107. // #ifdef H5
  108. // H5端,查询节点信息未计算导航栏高度
  109. res.bottom = Number(res.bottom) + 44
  110. // #endif
  111. if (Number(res.bottom) <= offset) handleRelativeTo(exposed, { boundingClientRect: res })
  112. })
  113. .catch((res) => {
  114. console.log(res)
  115. })
  116. }
  117. /**
  118. * 监听容器组件
  119. * @param {Object} exposed wd-sticky实例暴露出的事件
  120. * @param {Object} boundingClientRect 边界信息
  121. */
  122. function handleRelativeTo(exposed: any, { boundingClientRect }: any) {
  123. let childOffsetTop = exposed.offsetTop
  124. // #ifdef H5
  125. // H5端,导航栏为普通元素,需要将组件移动到导航栏的下边沿
  126. // H5的导航栏高度为44px
  127. childOffsetTop = childOffsetTop + 44
  128. // #endif
  129. const offset = exposed.stickyState.height + childOffsetTop
  130. let isAbsolute = boundingClientRect.bottom <= offset
  131. // #ifdef H5 || APP-PLUS
  132. isAbsolute = boundingClientRect.bottom < offset
  133. // #endif
  134. if (isAbsolute) {
  135. exposed.setPosition(true, 'absolute', boundingClientRect.height - exposed.stickyState.height)
  136. } else if (boundingClientRect.top <= offset && !isAbsolute) {
  137. if (exposed.stickyState.state === 'normal') return
  138. exposed.setPosition(false, 'fixed', childOffsetTop)
  139. }
  140. }
  141. </script>
  142. <style lang="scss" scoped>
  143. @import './index.scss';
  144. </style>