UNPKG

8.91 kBJavaScriptView Raw
1import React, { Component, cloneElement } from "react"
2import ClassNames from "classnames"
3import { getCompStyle } from "./utils"
4import docOffset from "./offset"
5import scroll from "window-scroll"
6
7function isValid(d) {
8 return d !== undefined && d !== null
9}
10
11function getNum(val) {
12 return val !== undefined ? parseInt(val) : 0
13}
14
15function getValidVal(...args) {
16 for (let i = 0; i < args.length; i++) {
17 if (isValid(args[i])) {
18 return args[i]
19 }
20 }
21}
22
23function isFn(val) {
24 return typeof val === "function"
25}
26
27class Sticky extends Component {
28 static defaultProps = {
29 edge: "bottom",
30 triggerDistance: 0
31 }
32
33 state = {
34 isSticky: false,
35 window: {
36 height: window.innerHeight,
37 width: window.innerWidth
38 }
39 }
40
41 StickyRef = React.createRef()
42
43 getContainerNode() {
44 let stickyContainer = this.StickyRef
45 this.container = stickyContainer && stickyContainer.current
46 return this.container
47 }
48
49 componentDidMount() {
50 this.getContainerNode()
51 this.initCloneContainerNode()
52 this.registerEvents()
53 setTimeout(() => {
54 this.initSticky()
55 })
56 }
57
58 initSticky() {
59 this.ifSticky(
60 () => {
61 this.sticky()
62 },
63 () => {
64 this.sticky(false)
65 }
66 )
67 }
68
69 componentWillUnmount() {
70 this.sticky(false)
71 this.cancelEvents()
72 setTimeout(() => {
73 this.wrapperNode.remove()
74 })
75 }
76
77 onScrollHandler(context) {
78 let { createState } = context.props
79 let handler =
80 context.bindHandler ||
81 function() {
82 requestAnimationFrame(() => {
83 if (createState) {
84 const state = createState()
85 if (state) {
86 context.setState(state)
87 }
88 }
89 context.ifSticky(
90 () => {
91 context.setState({
92 isSticky: true,
93 window: {
94 height: window.innerHeight,
95 width: window.innerWidth
96 }
97 })
98 context.sticky()
99 },
100 () => {
101 context.setState({
102 isSticky: false,
103 window: {
104 height: window.innerHeight,
105 width: window.innerWidth
106 }
107 })
108 context.sticky(false)
109 }
110 )
111 })
112 }
113 context.bindHandler = handler
114 return handler
115 }
116
117 setStyle(node, styles) {
118 let { style } = node
119 Object.keys(styles).forEach(name => {
120 style[name] = styles[name]
121 })
122 }
123
124 sticky(isSticky = true) {
125 let positionNode = this.getPositionNode()
126 let nodeData = this.getNodeData(positionNode)
127 let self = this
128 if (this.props.edge == "top") {
129 if (isSticky) {
130 this.setStyle(this.container, {
131 position: "fixed",
132 width: nodeData.width + "px",
133 height: nodeData.height + "px",
134 top: this.props.triggerDistance + "px",
135 left: nodeData.offsetLeft + "px",
136 zIndex: self.props.zIndex || "100000",
137 ...this.props.stickiedStyle
138 })
139 if (this.sticking) return
140 this.sticking = true
141 } else {
142 if (!this.sticking) return
143 self.setStyle(self.container, {
144 left: "",
145 zIndex: "",
146 width: "",
147 height: "",
148 position: "",
149 top: "",
150 ...self.props.unstickiedStyle
151 })
152
153 this.sticking = false
154 }
155 } else {
156 if (isSticky) {
157 this.setStyle(this.container, {
158 position: "fixed",
159 width: nodeData.width + "px",
160 height: nodeData.height + "px",
161 bottom: self.props.triggerDistance + "px",
162 left: nodeData.offsetLeft + "px",
163 zIndex: self.props.zIndex || "100000",
164 ...this.props.stickiedStyle
165 })
166 if (this.sticking) return
167 this.sticking = true
168 } else {
169 if (!this.sticking) return
170 this.setStyle(this.container, {
171 bottom: self.props.triggerDistance + "px"
172 })
173 let containerNode = this.container
174 self.setStyle(self.container, {
175 left: "",
176 zIndex: "",
177 width: "",
178 height: "",
179 position: "",
180 bottom: "",
181 ...self.props.unstickiedStyle
182 })
183 this.sticking = false
184 }
185 }
186 }
187
188 getPositionNode() {
189 let node = null
190 if (this.sticking) node = this.wrapperNode
191 else node = this.container
192 return node
193 }
194
195 ifSticky(ok, faild) {
196 let positionNode = this.getPositionNode()
197 let nodeData = this.getNodeData(positionNode)
198 let winData = this.getNodeData(window)
199 let self = this
200 let edge = self.props.edge
201 let getStickyBoundary = self.props.getStickyBoundary
202 let triggerDistance = self.props.triggerDistance
203 let isNotSticky = this.state.notSticky
204 if (isFn(getStickyBoundary)) {
205 if (!getStickyBoundary()) return faild.call(self)
206 }
207 if (isNotSticky) {
208 return faild.call(self)
209 }
210 if (edge != "top") {
211 if (
212 winData.scrollTop + winData.height <
213 nodeData.offsetTop + nodeData.height + triggerDistance
214 ) {
215 return ok.call(self)
216 }
217 } else {
218 if (winData.scrollTop > nodeData.offsetTop - triggerDistance) {
219 return ok.call(self)
220 }
221 }
222 faild.call(self)
223 }
224
225 getNodeData(node) {
226 let { clientHeight, clientWidth, innerHeight, innerWidth } = node
227 if (node !== window) {
228 let offset = docOffset(node)
229 let offsetLeft = offset ? offset.left : 0
230 let offsetTop = offset ? offset.top : 0
231 const rect = node.getBoundingClientRect()
232 const style = getCompStyle(node)
233 return {
234 offsetLeft: offsetLeft - getNum(style["margin-left"]),
235 offsetTop: offsetTop - getNum(style["margin-top"]),
236 width: rect.width,
237 height: rect.height
238 }
239 } else {
240 return {
241 height: window.innerHeight,
242 width: window.innerWidth,
243 scrollTop: window.pageYOffset,
244 scrollLeft: window.pageXOffset
245 }
246 }
247 return this.nodeData
248 }
249
250 initCloneContainerNode() {
251 if (this.wrapperNode) return this.wrapperNode
252 let oldNode = this.getContainerNode()
253 let nodeData = this.getNodeData(oldNode)
254 this.wrapperNode = document.createElement("div")
255 this.wrapperNode.style.height = nodeData.height + "px"
256 this.wrapperNode.classList.add("sticky-wrapper")
257 oldNode.parentNode.insertBefore(this.wrapperNode, oldNode)
258 this.wrapperNode.appendChild(oldNode)
259 }
260
261 cancelEvents() {
262 window.removeEventListener("scroll", this.onScrollHandler(this))
263 window.removeEventListener("resize", this.onScrollHandler(this))
264 }
265
266 registerEvents() {
267 window.addEventListener("scroll", this.onScrollHandler(this))
268 window.addEventListener("resize", this.onScrollHandler(this))
269 }
270
271 renderContainer() {
272 const { children } = this.props
273 return (
274 <div
275 ref={this.StickyRef}
276 className="sticky-container"
277 style={this.props.style}
278 >
279 {typeof children === "function"
280 ? children(this.state)
281 : children}
282 </div>
283 )
284 }
285
286 render() {
287 return this.renderContainer()
288 }
289}
290
291export default Sticky