Skip to content

Commit dbf819c

Browse files
kagolgitee-org
authored andcommitted
!86 新增sticky
Merge pull request !86 from 名难取/feature/add-sticky
2 parents f85326e + aa341a4 commit dbf819c

File tree

3 files changed

+412
-0
lines changed

3 files changed

+412
-0
lines changed

devui/sticky/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { App } from 'vue'
2+
import Sticky from './src/sticky'
3+
4+
Sticky.install = function(app: App): void {
5+
app.component(Sticky.name, Sticky)
6+
}
7+
8+
export { Sticky }
9+
10+
export default {
11+
title: 'Sticky 便贴',
12+
category: '通用',
13+
status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释
14+
install(app: App): void {
15+
16+
app.use(Sticky as any)
17+
}
18+
}

devui/sticky/src/sticky.tsx

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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

Comments
 (0)