|
| 1 | +import './sticky.scss' |
| 2 | + |
| 3 | +import { defineComponent, onMounted, reactive, ref, watch } from 'vue' |
| 4 | + |
| 5 | +export default defineComponent({ |
| 6 | + name: 'DSticky', |
| 7 | + props: { |
| 8 | + zIndex: { |
| 9 | + type: Number, |
| 10 | + }, |
| 11 | + container: { |
| 12 | + type: Element, |
| 13 | + default: '', |
| 14 | + }, |
| 15 | + view: { |
| 16 | + type: Object, |
| 17 | + default: ()=>{return {top:0,bottom:0}}, |
| 18 | + }, |
| 19 | + scrollTarget: { |
| 20 | + type: Element, |
| 21 | + default: '', |
| 22 | + }, |
| 23 | + }, |
| 24 | + emits: ['statusChange'], |
| 25 | + setup(props, ctx) { |
| 26 | + const { slots } = ctx |
| 27 | + let container: Element |
| 28 | + let scrollTarget: Element | Window |
| 29 | + |
| 30 | + let scrollTimer: any |
| 31 | + let scrollPreStart: number | null |
| 32 | + |
| 33 | + const THROTTLE_DELAY = 16; |
| 34 | + const THROTTLE_TRIGGER = 100; |
| 35 | + |
| 36 | + let parentNode: Element |
| 37 | + let containerLeft = 0 |
| 38 | + |
| 39 | + const state = reactive({ |
| 40 | + status: 'normal' |
| 41 | + }) |
| 42 | + |
| 43 | + watch( |
| 44 | + () => props.zIndex, |
| 45 | + () => { |
| 46 | + init() |
| 47 | + } |
| 48 | + ); |
| 49 | + watch( |
| 50 | + () => props.container, |
| 51 | + () => { |
| 52 | + init() |
| 53 | + } |
| 54 | + ); |
| 55 | + watch( |
| 56 | + () => props.scrollTarget, |
| 57 | + () => { |
| 58 | + init() |
| 59 | + } |
| 60 | + ); |
| 61 | + watch( |
| 62 | + () => state.status, |
| 63 | + () => { |
| 64 | + ctx.emit('statusChange', state.status) |
| 65 | + }, |
| 66 | + { immediate: true } |
| 67 | + ); |
| 68 | + |
| 69 | + const init = () => { |
| 70 | + parentNode = stickyRef.value.parentElement |
| 71 | + if (!props.container) { |
| 72 | + container = parentNode; |
| 73 | + } else { |
| 74 | + container = props.container |
| 75 | + } |
| 76 | + |
| 77 | + stickyRef.value.style.zIndex = props.zIndex |
| 78 | + |
| 79 | + scrollTarget = props.scrollTarget || window; |
| 80 | + scrollTarget.addEventListener('scroll',throttle); |
| 81 | + |
| 82 | + initScrollStatus(scrollTarget); |
| 83 | + } |
| 84 | + |
| 85 | + // 初始化,判断位置,如果有滚用动则用handler处理 |
| 86 | + const initScrollStatus = ( target: any)=>{ |
| 87 | + const scrollTargets = target === window ? |
| 88 | + [document.documentElement, document.body] : [target]; |
| 89 | + let flag = false; |
| 90 | + scrollTargets.forEach((scrollTarget) => { |
| 91 | + if (scrollTarget.scrollTop && scrollTarget.scrollTop > 0) { |
| 92 | + flag = true; |
| 93 | + } |
| 94 | + }); |
| 95 | + if (flag) { |
| 96 | + setTimeout(scrollHandler); |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + |
| 101 | + const statusProcess = (status: any) => { |
| 102 | + const wrapper = stickyRef.value|| document.createElement('div') |
| 103 | + switch (status) { |
| 104 | + case 'normal': |
| 105 | + wrapper.style.top = 'auto'; |
| 106 | + wrapper.style.left = 'auto'; |
| 107 | + wrapper.style.position = 'static'; |
| 108 | + break; |
| 109 | + case 'follow': |
| 110 | + const scrollTargetElement:any = scrollTarget |
| 111 | + const viewOffset = scrollTarget && scrollTarget !== window ? |
| 112 | + scrollTargetElement.getBoundingClientRect().top : 0; |
| 113 | + wrapper.style.top = +viewOffset + ((props.view && props.view.top) || 0) + 'px'; |
| 114 | + wrapper.style.left = wrapper.getBoundingClientRect().left + 'px'; |
| 115 | + wrapper.style.position = 'fixed'; |
| 116 | + break; |
| 117 | + case 'stay': |
| 118 | + wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px'; |
| 119 | + wrapper.style.left = 'auto'; |
| 120 | + wrapper.style.position = 'relative'; |
| 121 | + break; |
| 122 | + case 'remain': |
| 123 | + if (wrapper.style.position !== 'fixed' && wrapper.style.position !== 'absolute') { |
| 124 | + wrapper.style.top = calculateRelativePosition(wrapper, parentNode, 'top') + 'px'; |
| 125 | + wrapper.style.left = 'auto'; |
| 126 | + wrapper.style.position = 'absolute'; |
| 127 | + } |
| 128 | + wrapper.style.top = |
| 129 | + calculateRemainPosition(wrapper, parentNode, container) + 'px'; |
| 130 | + wrapper.style.left = calculateRelativePosition(wrapper, parentNode, 'left') + 'px'; |
| 131 | + wrapper.style.position = 'relative'; |
| 132 | + break; |
| 133 | + default: |
| 134 | + break; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + const throttle = () => { |
| 139 | + const fn = scrollAndResizeHock; |
| 140 | + const time = Date.now(); |
| 141 | + if (scrollTimer) { |
| 142 | + clearTimeout(scrollTimer); |
| 143 | + } |
| 144 | + if (!scrollPreStart) { |
| 145 | + scrollPreStart = time; |
| 146 | + } |
| 147 | + if (time - scrollPreStart > THROTTLE_TRIGGER) { |
| 148 | + fn(); |
| 149 | + scrollPreStart = null; |
| 150 | + scrollTimer = null; |
| 151 | + } else { |
| 152 | + scrollTimer = setTimeout(() => { |
| 153 | + fn(); |
| 154 | + scrollPreStart = null; |
| 155 | + scrollTimer = null; |
| 156 | + }, THROTTLE_DELAY); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + const scrollAndResizeHock = () => { |
| 161 | + if (container.getBoundingClientRect().left - (containerLeft || 0) !== 0) { |
| 162 | + state.status = 'stay'; |
| 163 | + containerLeft = container.getBoundingClientRect().left; |
| 164 | + } else { |
| 165 | + scrollHandler(); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + const scrollHandler = () => { |
| 170 | + const scrollTargetElement:any = scrollTarget |
| 171 | + const wrapper = stickyRef.value || document.createElement('div') |
| 172 | + const viewOffsetTop = scrollTarget && scrollTarget !== window ? |
| 173 | + scrollTargetElement.getBoundingClientRect().top : 0; |
| 174 | + const computedStyle = window.getComputedStyle(container); |
| 175 | + if (parentNode.getBoundingClientRect().top - viewOffsetTop > ((props.view && props.view.top) || 0)) { |
| 176 | + state.status = 'normal'; |
| 177 | + statusProcess(state.status); |
| 178 | + } else if ( |
| 179 | + container.getBoundingClientRect().top + |
| 180 | + parseInt(computedStyle.paddingTop, 10) + |
| 181 | + parseInt(computedStyle.borderTopWidth, 10) - |
| 182 | + viewOffsetTop >= |
| 183 | + ((props.view && props.view.top) || 0) |
| 184 | + ) { |
| 185 | + state.status = 'normal'; |
| 186 | + statusProcess(state.status); |
| 187 | + } else if ( |
| 188 | + container.getBoundingClientRect().bottom - |
| 189 | + parseInt(computedStyle.paddingBottom, 10) - |
| 190 | + parseInt(computedStyle.borderBottomWidth, 10) < |
| 191 | + viewOffsetTop + |
| 192 | + ((props.view && props.view.top) || 0) + |
| 193 | + wrapper.getBoundingClientRect().height + |
| 194 | + ((props.view && props.view.bottom) || 0) |
| 195 | + ) { |
| 196 | + state.status = 'remain'; |
| 197 | + statusProcess(state.status); |
| 198 | + } else if ( |
| 199 | + container.getBoundingClientRect().top + parseInt(computedStyle.paddingTop, 10) - viewOffsetTop < |
| 200 | + ((props.view && props.view.top) || 0) |
| 201 | + ) { |
| 202 | + state.status = 'follow'; |
| 203 | + statusProcess(state.status); |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + |
| 208 | + const calculateRelativePosition =(element:any, relativeElement:any, direction:'left' | 'top') => { |
| 209 | + const key = { |
| 210 | + left: ['left', 'Left'], |
| 211 | + top: ['top', 'Top'], |
| 212 | + }; |
| 213 | + if (window && window.getComputedStyle) { |
| 214 | + const computedStyle = window.getComputedStyle(relativeElement); |
| 215 | + return ( |
| 216 | + element.getBoundingClientRect()[key[direction][0]] - |
| 217 | + relativeElement.getBoundingClientRect()[key[direction][0]] - |
| 218 | + parseInt(computedStyle[ direction === 'left' ? 'paddingLeft' : 'paddingTop' ], 10) - |
| 219 | + parseInt(computedStyle[ direction === 'left' ? 'borderLeftWidth' : 'borderTopWidth' ], 10) |
| 220 | + ); |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + const calculateRemainPosition = (element: any, relativeElement: any, container: any) => { |
| 225 | + if (window && window.getComputedStyle) { |
| 226 | + const computedStyle = window.getComputedStyle(container); |
| 227 | + const result = |
| 228 | + container.getBoundingClientRect().height - |
| 229 | + element.getBoundingClientRect().height + |
| 230 | + container.getBoundingClientRect().top - |
| 231 | + relativeElement.getBoundingClientRect().top - |
| 232 | + parseInt(computedStyle['paddingTop'], 10) - |
| 233 | + parseInt(computedStyle['borderTopWidth'], 10) - |
| 234 | + parseInt(computedStyle['paddingBottom'], 10) - |
| 235 | + parseInt(computedStyle['borderBottomWidth'], 10); |
| 236 | + return result < 0 ? 0 : result; |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + onMounted(() => { |
| 241 | + init() |
| 242 | + }) |
| 243 | + |
| 244 | + const stickyRef = ref() |
| 245 | + |
| 246 | + |
| 247 | + return () => { |
| 248 | + return ( |
| 249 | + <div ref={stickyRef}> |
| 250 | + { slots.default ? slots.default() : '' } |
| 251 | + </div> |
| 252 | + ) |
| 253 | + } |
| 254 | + } |
| 255 | +}) |
0 commit comments