houzhongjian
2024-08-08 820397e43a0b64d35c6d31d2a55475061438593b
提交 | 用户 | 时间
820397 1 <script lang="ts" setup>
H 2 import { propTypes } from '@/utils/propTypes'
3 import { isClient, useEventListener, useWindowSize } from '@vueuse/core'
4 import type { CSSProperties } from 'vue'
5
6 defineOptions({ name: 'Sticky' })
7
8 const props = defineProps({
9   // 距离顶部或者底部的距离(单位px)
10   offset: propTypes.number.def(0),
11   // 设置元素的堆叠顺序
12   zIndex: propTypes.number.def(999),
13   // 设置指定的class
14   className: propTypes.string.def(''),
15   // 定位方式,默认为(top),表示距离顶部位置,可以设置为top或者bottom
16   position: {
17     type: String,
18     validator: function (value: string) {
19       return ['top', 'bottom'].indexOf(value) !== -1
20     },
21     default: 'top'
22   }
23 })
24 const width = ref('auto' as string)
25 const height = ref('auto' as string)
26 const isSticky = ref(false)
27 const refSticky = shallowRef<HTMLElement>()
28 const scrollContainer = shallowRef<HTMLElement | Window>()
29 const { height: windowHeight } = useWindowSize()
30 onMounted(() => {
31   height.value = refSticky.value?.getBoundingClientRect().height + 'px'
32
33   scrollContainer.value = getScrollContainer(refSticky.value!, true)
34   useEventListener(scrollContainer, 'scroll', handleScroll)
35   useEventListener('resize', handleResize)
36   handleScroll()
37 })
38 onActivated(() => {
39   handleScroll()
40 })
41
42 const camelize = (str: string): string => {
43   return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
44 }
45
46 const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
47   if (!isClient || !element || !styleName) return ''
48
49   let key = camelize(styleName)
50   if (key === 'float') key = 'cssFloat'
51   try {
52     const style = element.style[styleName]
53     if (style) return style
54     const computed = document.defaultView?.getComputedStyle(element, '')
55     return computed ? computed[styleName] : ''
56   } catch {
57     return element.style[styleName]
58   }
59 }
60 const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
61   if (!isClient) return false
62   const key = (
63     {
64       undefined: 'overflow',
65       true: 'overflow-y',
66       false: 'overflow-x'
67     } as const
68   )[String(isVertical)]!
69   const overflow = getStyle(el, key)
70   return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
71 }
72
73 const getScrollContainer = (
74   el: HTMLElement,
75   isVertical: boolean
76 ): Window | HTMLElement | undefined => {
77   if (!isClient) return
78   let parent = el
79   while (parent) {
80     if ([window, document, document.documentElement].includes(parent)) return window
81     if (isScroll(parent, isVertical)) return parent
82     parent = parent.parentNode as HTMLElement
83   }
84   return parent
85 }
86
87 const handleScroll = () => {
88   width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
89   if (props.position === 'top') {
90     const offsetTop = refSticky.value?.getBoundingClientRect().top
91     if (offsetTop !== undefined && offsetTop < props.offset) {
92       sticky()
93       return
94     }
95     reset()
96   } else {
97     const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
98
99     if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
100       sticky()
101       return
102     }
103     reset()
104   }
105 }
106 const handleResize = () => {
107   if (isSticky.value && refSticky.value) {
108     width.value = refSticky.value.getBoundingClientRect().width + 'px'
109   }
110 }
111 const sticky = () => {
112   if (isSticky.value) {
113     return
114   }
115   isSticky.value = true
116 }
117 const reset = () => {
118   if (!isSticky.value) {
119     return
120   }
121   width.value = 'auto'
122   isSticky.value = false
123 }
124 </script>
125 <template>
126   <div ref="refSticky" :style="{ height: height, zIndex: zIndex }">
127     <div
128       :class="className"
129       :style="{
130         top: position === 'top' ? offset + 'px' : '',
131         bottom: position !== 'top' ? offset + 'px' : '',
132         zIndex: zIndex,
133         position: isSticky ? 'fixed' : 'static',
134         width: width,
135         height: height
136       }"
137     >
138       <slot>
139         <div>sticky</div>
140       </slot>
141     </div>
142   </div>
143 </template>