-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
345 lines (300 loc) · 9.02 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
'use strict';
import { Component } from 'react';
import { findDOMNode } from 'react-dom';
const stickyAPI = {
list: [],
_inited_: false,
init(rootDOM = document) {
if (!this._inited_) {
this._inited_ = true;
rootDOM.addEventListener("scroll", () => {
this.handleSticky();
}, { passive: true, capture: true });
}
},
handleSticky(forceReflow = false) {
const { list } = this;
list.forEach((node, index) => {
const { dom, ref } = node;
if (dom) {
const rect = node.dom.getBoundingClientRect();
const prevNode = list[index - 1];
const prevRect = prevNode ? prevNode.dom.getBoundingClientRect() : { top: 0, height: 0 };
const prevBottom = prevRect.top + prevRect.height;
ref.onStickyScroll({
forceReflow,
// 距离顶上
top: rect.top,
prevTop: prevRect.top,
prevBottom,
// 距离前一个吸顶的距离
prevTopDiff: index === 0 ? rect.top : rect.top - prevBottom,
self: node,
list,
index,
stickyAPI: this
});
}
});
},
add(ref) {
if (!this.list.find(n => n.ref === ref)) {
const placeholderDOM = document.createElement('div');
placeholderDOM.setAttribute('sticky-role', 'placeholder');
this.list.push({
ref,
// 占位元素
placeholderDOM,
// 占位元素是否添加到dom中
isPlaceholderIn: false,
// 吸顶前的样式
_setStickyStyle_: null,
get dom() {
const dom = findDOMNode(this.ref);
return dom && dom.nodeType === 1 ? dom : null;
},
/**是否应该从吸顶状态回到非吸顶状态
* 占位元素的绝对位置大于吸顶元素,解除吸顶状态
*/
get keepSticky() {
const { dom, placeholderDOM, isPlaceholderIn } = this;
if (dom && isPlaceholderIn) {
return placeholderDOM.getBoundingClientRect().top <= dom.getBoundingClientRect().top;
}
return true;
},
addPlaceholder() {
const { dom, placeholderDOM } = this;
if (!this.isPlaceholderIn && dom && dom.parentNode) {
dom.parentNode.insertBefore(placeholderDOM, dom);
this.isPlaceholderIn = true;
}
},
removePlaceholder() {
const { dom, placeholderDOM } = this;
if (this.isPlaceholderIn && dom && dom.parentNode) {
dom.parentNode.removeChild(placeholderDOM);
this.isPlaceholderIn = false;
}
},
});
this.sort();
}
},
remove(ref) {
const index = this.list.findIndex(node => node.ref === ref);
if (index > -1) {
this.list.splice(index, 1);
// 移除占位元素
const node = this.list[index];
node.removePlaceholder();
}
},
find(ref) {
return this.list.find(node => node.ref === ref);
},
sort() {
return this.list.sort((a, b) => {
try {
const v = a.dom.compareDocumentPosition(b.dom);
if (v === 4 || v === 20) { // a在b之前
return -1;
} else if (v === 2 || v === 10) { // a在b之后
return 1;
}
} catch (e) {
return 1;
}
});
},
// 置为吸顶状态。会自动保存吸顶前的样式,便于后面回到吸顶前的状态
setSticky(node, style = {}, forceReflow = false) {
// 从未设置吸顶转为吸顶,存储吸顶前的样式
if (forceReflow || !node.isSticky) {
node.isSticky = true;
const { dom, placeholderDOM } = node;
// 向下滚动,当前组件从吸顶还原回正常状态,由于前置的组件还是吸顶状态不占高度,当前组件会上移。创建一个占位元素
this._updatePlaceholder(placeholderDOM, dom);
const styleNames = Object.keys(style);
const stickyStyle = {};
for (const styleName of styleNames) {
stickyStyle[styleName] = dom.style[styleName];
dom.style[styleName] = style[styleName];
}
node.addPlaceholder();
node._setStickyStyle_ = stickyStyle;
}
},
// 还原回吸顶前的状态
resetSticky(node) {
node.isSticky = false;
const { _setStickyStyle_ } = node;
// 还原吸顶前的样式状态
if (_setStickyStyle_) {
const { dom } = node;
const styleNames = Object.keys(_setStickyStyle_);
for (const styleName of styleNames) {
dom.style[styleName] = _setStickyStyle_[styleName];
}
node.removePlaceholder();
node._setStickyStyle_ = null;
}
},
/**默认吸顶策略
* @param {*} helper
*/
dynamicStickyScroll(helper) {
const {
prevTopDiff, // 和前一个吸顶组件之间的距离
prevBottom, // 前一个吸顶组件的底部边界在视口的位置
self, // 吸顶队列中的自己
stickyAPI, // 吸顶相关的API
forceReflow
} = helper;
const {
dom, // 组件的dom元素
isSticky // 是否吸顶
} = self;
if (isSticky) {
if (!self.keepSticky) { // 是否该解除吸顶了
stickyAPI.resetSticky(self);
} else if (dom.getBoundingClientRect().top != prevBottom) {
// 吸顶后位置发生偏差,进行二次校准
// 有时候前一个元素一边吸顶一边高度发生变化,后面的元素需要不断修改吸顶位置
dom.style.top = `${prevBottom}px`;
}
} else if (prevTopDiff <= 0) { // 距离上个吸顶元素小于等于0
stickyAPI.setSticky(self, { // 调用吸顶设置API
position: 'fixed',
zIndex: 100,
top: `${prevBottom}px`,
...self.ref.getStickyStyle(helper), // 获取用户自定义吸顶样式
}, forceReflow);
}
},
/**
* 更新占位dom的样式,默认重新获取高度
*/
_updatePlaceholder(placeholderDOM, dom, styleNames = ['position', 'top', 'left', 'bottom', 'right', 'display', 'margin', 'padding', 'width', 'height', 'border']) {
if (placeholderDOM && dom) {
let domStyle = {};
try {
domStyle = getComputedStyle(dom);
} catch (e) {}
for (const key of styleNames) {
placeholderDOM.style[key] = domStyle[key];
}
placeholderDOM.style.opacity = 0;
}
},
// 对外暴露用来动态调整
updatePlaceholder(ref, styleNames = ['height']) {
const node = this.find(ref);
if (node) {
this._updatePlaceholder(node.placeholderDOM, node.dom, styleNames);
}
}
};
const defaultMembers = {
stickyAPI,
autoSticky: true,
autoStickyTrigger: true,
onStickyScroll: stickyAPI.dynamicStickyScroll,
getStickyStyle() {
return {};
}
};
class Sticky extends Component {
static stickyRootDOM = document;
stickyAPI = defaultMembers.stickyAPI;
autoSticky = defaultMembers.autoSticky;
autoStickyTrigger = defaultMembers.autoStickyTrigger;
constructor(props) {
super(props);
const { stickyAPI } = this;
stickyAPI.init(Sticky.stickyRootDOM);
setTimeout(() => {
if (this.autoSticky) {
stickyAPI.add(this);
if (this.autoStickyTrigger) {
stickyAPI.handleSticky();
}
}
});
}
componentWillUnmount() {
this.stickyAPI.remove(this);
}
getStickyStyle(...args){
return defaultMembers.getStickyStyle(...args);
}
onStickyScroll(...args) {
return defaultMembers.onStickyScroll(...args);
}
}
function setProps(target, props, fields = ['autoSticky', 'autoStickyTrigger', 'getStickyStyle', 'onStickyScroll']) {
const obj = {};
fields.forEach(field => {
if (props.hasOwnProperty(field) && props[field] !== undefined) {
obj[field] = props[field];
}
});
Object.assign(target, obj);
}
class StickyView extends Sticky {
constructor(props) {
super(props);
stickyAPI.init(Sticky.stickyRootDOM);
setProps(this, props);
}
componentWillReceiveProps(nextProps) {
setProps(this, nextProps, ['getStickyStyle', 'onStickyScroll']);
}
render() {
return this.props.children;
}
}
// hooks
function createStickyRef(props) {
stickyAPI.init(Sticky.stickyRootDOM);
return {
_current: null,
set current(current) {
if (!current) {
// 移除
stickyAPI.remove(this._current);
} else {
if (!this._current) {
// 从无到有,加入吸顶队列,合成吸顶API
addHookRef(current, props);
} else if (this._current !== current) {
// 新加入的实例和之前的不一样,先移除旧实例,再添加新实例
stickyAPI.remove(this._current);
addHookRef(current, props);
}
}
this._current = current;
},
get current() {
return this._current;
}
};
}
function addHookRef(ref, props) {
if (ref) {
Object.assign(ref, defaultMembers);
setProps(ref, props);
stickyAPI.add(ref);
setTimeout(() => {
if (ref.autoStickyTrigger) {
stickyAPI.handleSticky();
}
});
}
return ref;
}
export {
StickyView,
createStickyRef,
};
export default Sticky;