1 |
|
2 | import { useEffect, useState } from "react";
|
3 | import { jsx } from "react/jsx-runtime";
|
4 | var getScrollParent = (node) => {
|
5 | let parent = node;
|
6 | while (parent = parent.parentElement) {
|
7 | const overflowYVal = getComputedStyle(parent, null).getPropertyValue("overflow-y");
|
8 | if (parent === document.body)
|
9 | return window;
|
10 | if (overflowYVal === "auto" || overflowYVal === "scroll" || overflowYVal === "overlay") {
|
11 | return parent;
|
12 | }
|
13 | }
|
14 | return window;
|
15 | };
|
16 | var isOffsetElement = (el) => el.firstChild ? el.firstChild.offsetParent === el : true;
|
17 | var offsetTill = (node, target) => {
|
18 | let current = node;
|
19 | let offset = 0;
|
20 | if (!isOffsetElement(target)) {
|
21 | offset += node.offsetTop - target.offsetTop;
|
22 | target = node.offsetParent;
|
23 | offset += -node.offsetTop;
|
24 | }
|
25 | do {
|
26 | offset += current.offsetTop;
|
27 | current = current.offsetParent;
|
28 | } while (current && current !== target);
|
29 | return offset;
|
30 | };
|
31 | var getParentNode = (node) => {
|
32 | let currentParent = node.parentElement;
|
33 | while (currentParent) {
|
34 | const style = getComputedStyle(currentParent, null);
|
35 | if (style.getPropertyValue("display") !== "contents")
|
36 | break;
|
37 | currentParent = currentParent.parentElement;
|
38 | }
|
39 | return currentParent || window;
|
40 | };
|
41 | var stickyProp = null;
|
42 | if (typeof CSS !== "undefined" && CSS.supports) {
|
43 | if (CSS.supports("position", "sticky"))
|
44 | stickyProp = "sticky";
|
45 | else if (CSS.supports("position", "-webkit-sticky"))
|
46 | stickyProp = "-webkit-sticky";
|
47 | }
|
48 | var passiveArg = false;
|
49 | try {
|
50 | const opts = Object.defineProperty({}, "passive", {
|
51 |
|
52 | get() {
|
53 | passiveArg = { passive: true };
|
54 | }
|
55 | });
|
56 | const emptyHandler = () => {
|
57 | };
|
58 | window.addEventListener("testPassive", emptyHandler, opts);
|
59 | window.removeEventListener("testPassive", emptyHandler, opts);
|
60 | } catch (e) {
|
61 | }
|
62 | var getDimensions = (opts) => {
|
63 | const { el, onChange, unsubs, measure } = opts;
|
64 | if (el === window) {
|
65 | const getRect = () => ({ top: 0, left: 0, height: window.innerHeight, width: window.innerWidth });
|
66 | const mResult = measure(getRect());
|
67 | const handler = () => {
|
68 | Object.assign(mResult, measure(getRect()));
|
69 | onChange();
|
70 | };
|
71 | window.addEventListener("resize", handler, passiveArg);
|
72 | unsubs.push(() => window.removeEventListener("resize", handler));
|
73 | return mResult;
|
74 | } else {
|
75 | const mResult = measure(el.getBoundingClientRect());
|
76 | const handler = () => {
|
77 | Object.assign(mResult, measure(el.getBoundingClientRect()));
|
78 | onChange();
|
79 | };
|
80 | const ro = new ResizeObserver(handler);
|
81 | ro.observe(el);
|
82 | unsubs.push(() => ro.disconnect());
|
83 | return mResult;
|
84 | }
|
85 | };
|
86 | var getVerticalPadding = (node) => {
|
87 | const computedParentStyle = getComputedStyle(node, null);
|
88 | const parentPaddingTop = parseInt(computedParentStyle.getPropertyValue("padding-top"), 10);
|
89 | const parentPaddingBottom = parseInt(computedParentStyle.getPropertyValue("padding-bottom"), 10);
|
90 | return { top: parentPaddingTop, bottom: parentPaddingBottom };
|
91 | };
|
92 | var setup = (node, unsubs, opts) => {
|
93 | const { bottom, offsetBottom, offsetTop } = opts;
|
94 | const scrollPane = getScrollParent(node);
|
95 | let isScheduled = false;
|
96 | const scheduleOnLayout = () => {
|
97 | if (!isScheduled) {
|
98 | requestAnimationFrame(() => {
|
99 | const nextMode = onLayout();
|
100 | if (nextMode !== mode)
|
101 | changeMode(nextMode);
|
102 | isScheduled = false;
|
103 | });
|
104 | }
|
105 | isScheduled = true;
|
106 | };
|
107 | let latestScrollY = scrollPane === window ? window.scrollY : scrollPane.scrollTop;
|
108 | const isBoxTooLow = (scrollY) => {
|
109 | const { offsetTop: scrollPaneOffset, height: viewPortHeight } = scrollPaneDims;
|
110 | const { naturalTop } = parentDims;
|
111 | const { height: nodeHeight } = nodeDims;
|
112 | if (scrollY + scrollPaneOffset + viewPortHeight >= naturalTop + nodeHeight + relativeOffset + offsetBottom) {
|
113 | return true;
|
114 | }
|
115 | return false;
|
116 | };
|
117 | const onLayout = () => {
|
118 | const { height: viewPortHeight } = scrollPaneDims;
|
119 | const { height: nodeHeight } = nodeDims;
|
120 | if (nodeHeight + offsetTop + offsetBottom <= viewPortHeight) {
|
121 | return 3 ;
|
122 | } else {
|
123 | if (isBoxTooLow(latestScrollY)) {
|
124 | return 1 ;
|
125 | } else {
|
126 | return 2 ;
|
127 | }
|
128 | }
|
129 | };
|
130 | const scrollPaneIsOffsetEl = scrollPane !== window && isOffsetElement(scrollPane);
|
131 | const scrollPaneDims = getDimensions({
|
132 | el: scrollPane,
|
133 | onChange: scheduleOnLayout,
|
134 | unsubs,
|
135 | measure: ({ height, top }) => ({
|
136 | height,
|
137 | offsetTop: scrollPaneIsOffsetEl ? top : 0
|
138 | })
|
139 | });
|
140 | const parentNode = getParentNode(node);
|
141 | const parentPaddings = parentNode === window ? { top: 0, bottom: 0 } : getVerticalPadding(parentNode);
|
142 | const parentDims = getDimensions({
|
143 | el: parentNode,
|
144 | onChange: scheduleOnLayout,
|
145 | unsubs,
|
146 | measure: ({ height }) => ({
|
147 | height: height - parentPaddings.top - parentPaddings.bottom,
|
148 | naturalTop: parentNode === window ? 0 : offsetTill(parentNode, scrollPane) + parentPaddings.top + scrollPaneDims.offsetTop
|
149 | })
|
150 | });
|
151 | const nodeDims = getDimensions({
|
152 | el: node,
|
153 | onChange: scheduleOnLayout,
|
154 | unsubs,
|
155 | measure: ({ height }) => ({ height })
|
156 | });
|
157 | let relativeOffset = 0;
|
158 | let mode = onLayout();
|
159 | const changeMode = (newMode) => {
|
160 | const prevMode = mode;
|
161 | mode = newMode;
|
162 | if (prevMode === 2 )
|
163 | relativeOffset = -1;
|
164 | if (newMode === 3 ) {
|
165 | node.style.position = stickyProp;
|
166 | if (bottom) {
|
167 | node.style.bottom = `${offsetBottom}px`;
|
168 | } else {
|
169 | node.style.top = `${offsetTop}px`;
|
170 | }
|
171 | return;
|
172 | }
|
173 | const { height: viewPortHeight, offsetTop: scrollPaneOffset } = scrollPaneDims;
|
174 | const { height: parentHeight, naturalTop } = parentDims;
|
175 | const { height: nodeHeight } = nodeDims;
|
176 | if (newMode === 2 ) {
|
177 | node.style.position = "relative";
|
178 | relativeOffset = prevMode === 0 ? Math.max(0, scrollPaneOffset + latestScrollY - naturalTop + offsetTop) : Math.max(
|
179 | 0,
|
180 | scrollPaneOffset + latestScrollY + viewPortHeight - (naturalTop + nodeHeight + offsetBottom)
|
181 | );
|
182 | if (bottom) {
|
183 | const nextBottom = Math.max(0, parentHeight - nodeHeight - relativeOffset);
|
184 | node.style.bottom = `${nextBottom}px`;
|
185 | } else {
|
186 | node.style.top = `${relativeOffset}px`;
|
187 | }
|
188 | } else {
|
189 | node.style.position = stickyProp;
|
190 | if (newMode === 1 ) {
|
191 | if (bottom) {
|
192 | node.style.bottom = `${offsetBottom}px`;
|
193 | } else {
|
194 | node.style.top = `${viewPortHeight - nodeHeight - offsetBottom}px`;
|
195 | }
|
196 | } else {
|
197 | if (bottom) {
|
198 | node.style.bottom = `${viewPortHeight - nodeHeight - offsetBottom}px`;
|
199 | } else {
|
200 | node.style.top = `${offsetTop}px`;
|
201 | }
|
202 | }
|
203 | }
|
204 | };
|
205 | changeMode(mode);
|
206 | const onScroll = (scrollY) => {
|
207 | if (scrollY === latestScrollY)
|
208 | return;
|
209 | const scrollDelta = scrollY - latestScrollY;
|
210 | latestScrollY = scrollY;
|
211 | if (mode === 3 )
|
212 | return;
|
213 | const { offsetTop: scrollPaneOffset, height: viewPortHeight } = scrollPaneDims;
|
214 | const { naturalTop, height: parentHeight } = parentDims;
|
215 | const { height: nodeHeight } = nodeDims;
|
216 | if (scrollDelta > 0) {
|
217 | if (mode === 0 ) {
|
218 | if (scrollY + scrollPaneOffset + offsetTop > naturalTop) {
|
219 | const topOffset = Math.max(0, scrollPaneOffset + latestScrollY - naturalTop + offsetTop);
|
220 | if (scrollY + scrollPaneOffset + viewPortHeight <= naturalTop + nodeHeight + topOffset + offsetBottom) {
|
221 | changeMode(2 );
|
222 | } else {
|
223 | changeMode(1 );
|
224 | }
|
225 | }
|
226 | } else if (mode === 2 ) {
|
227 | if (isBoxTooLow(scrollY))
|
228 | changeMode(1 );
|
229 | }
|
230 | } else {
|
231 | if (mode === 1 ) {
|
232 | if (scrollPaneOffset + scrollY + viewPortHeight < naturalTop + parentHeight + offsetBottom) {
|
233 | const bottomOffset = Math.max(
|
234 | 0,
|
235 | scrollPaneOffset + latestScrollY + viewPortHeight - (naturalTop + nodeHeight + offsetBottom)
|
236 | );
|
237 | if (scrollPaneOffset + scrollY + offsetTop >= naturalTop + bottomOffset) {
|
238 | changeMode(2 );
|
239 | } else {
|
240 | changeMode(0 );
|
241 | }
|
242 | }
|
243 | } else if (mode === 2 ) {
|
244 | if (scrollPaneOffset + scrollY + offsetTop < naturalTop + relativeOffset) {
|
245 | changeMode(0 );
|
246 | }
|
247 | }
|
248 | }
|
249 | };
|
250 | const handleScroll = scrollPane === window ? () => onScroll(window.scrollY) : () => onScroll(scrollPane.scrollTop);
|
251 | scrollPane.addEventListener("scroll", handleScroll, passiveArg);
|
252 | scrollPane.addEventListener("mousewheel", handleScroll, passiveArg);
|
253 | unsubs.push(
|
254 | () => scrollPane.removeEventListener("scroll", handleScroll),
|
255 | () => scrollPane.removeEventListener("mousewheel", handleScroll)
|
256 | );
|
257 | };
|
258 | var useStickyBox = ({
|
259 | offsetTop = 0,
|
260 | offsetBottom = 0,
|
261 | bottom = false
|
262 | } = {}) => {
|
263 | const [node, setNode] = useState(null);
|
264 | useEffect(() => {
|
265 | if (!node || !stickyProp)
|
266 | return;
|
267 | const unsubs = [];
|
268 | setup(node, unsubs, { offsetBottom, offsetTop, bottom });
|
269 | return () => {
|
270 | unsubs.forEach((fn) => fn());
|
271 | };
|
272 | }, [node, offsetBottom, offsetTop, bottom]);
|
273 | return setNode;
|
274 | };
|
275 | var StickyBox = (props) => {
|
276 | const { offsetTop, offsetBottom, bottom, children, className, style } = props;
|
277 | const ref = useStickyBox({ offsetTop, offsetBottom, bottom });
|
278 | return jsx("div", { className, style, ref, children });
|
279 | };
|
280 | var src_default = StickyBox;
|
281 | export {
|
282 | src_default as default,
|
283 | useStickyBox
|
284 | };
|