homeflowjs 1.0.75 → 1.0.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,13 @@
|
|
1
1
|
import { useState, useEffect, useRef } from 'react';
|
2
|
-
import { isFunction } from '../utils';
|
3
|
-
import useIntersectionObserver from './use-intersection-observer'
|
2
|
+
import { isFunction, createMutationObserver } from '../utils';
|
3
|
+
import useIntersectionObserver from './use-intersection-observer';
|
4
4
|
|
5
5
|
const getTargetElementHelper = (target) => {
|
6
6
|
// If target is chunk ID
|
7
7
|
let targetElement;
|
8
8
|
if (!isNaN(target)) {
|
9
|
-
targetElement = document.querySelector(
|
10
|
-
|
11
|
-
);
|
9
|
+
targetElement = document.querySelector(`[data-content-id="${target}"]`);
|
10
|
+
if (!targetElement) return;
|
12
11
|
}
|
13
12
|
|
14
13
|
// Check for ID or class name
|
@@ -16,11 +15,20 @@ const getTargetElementHelper = (target) => {
|
|
16
15
|
if (!targetElement) targetElement = document.querySelector(`#${target}`);
|
17
16
|
|
18
17
|
return targetElement;
|
19
|
-
}
|
18
|
+
};
|
19
|
+
|
20
|
+
const scrollToTarget = (currentTarget = {}) => {
|
21
|
+
const {
|
22
|
+
hash,
|
23
|
+
targetElement,
|
24
|
+
scrollAmount = 0,
|
25
|
+
yOffset = 0,
|
26
|
+
additionalYOffset = 0,
|
27
|
+
} = currentTarget;
|
20
28
|
|
21
|
-
|
22
|
-
if(!targetElement) return;
|
29
|
+
if (!targetElement) return;
|
23
30
|
|
31
|
+
// Calculate vertical offset
|
24
32
|
let verticalOffset = 0;
|
25
33
|
if (typeof yOffset === 'number') {
|
26
34
|
verticalOffset = yOffset;
|
@@ -30,22 +38,33 @@ const scrollToTarget = ({ targetElement, yOffset = 0, additionalYOffset = 0 } =
|
|
30
38
|
verticalOffset = value;
|
31
39
|
}
|
32
40
|
}
|
33
|
-
|
34
|
-
|
41
|
+
|
42
|
+
if (additionalYOffset) {
|
43
|
+
verticalOffset += parseInt(additionalYOffset, 10);
|
35
44
|
}
|
36
45
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
* number of scrolled pixels to the element’s window location using scrollY:
|
43
|
-
*/
|
44
|
-
const rect = targetElement.getBoundingClientRect();
|
46
|
+
// If verticalOffset is negative, add it instead of subtracting
|
47
|
+
const scrollTop = verticalOffset < 0
|
48
|
+
? scrollAmount + Math.abs(verticalOffset)
|
49
|
+
: scrollAmount - verticalOffset;
|
50
|
+
|
45
51
|
window.scroll({
|
46
|
-
top:
|
52
|
+
top: scrollTop,
|
53
|
+
behavior: 'smooth',
|
54
|
+
});
|
55
|
+
|
56
|
+
// Update the URL hash
|
57
|
+
if (hash) window.location.hash = hash;
|
58
|
+
};
|
59
|
+
|
60
|
+
/**
|
61
|
+
* This is used to continuously scroll to the bottom of page.
|
62
|
+
*/
|
63
|
+
const scrollToBottom = () => {
|
64
|
+
window.scroll({
|
65
|
+
top: document.body.scrollHeight,
|
66
|
+
left: 0,
|
47
67
|
behavior: 'smooth',
|
48
|
-
...(signal && { signal: signal }), // This is used to abort the scroll via a trigger in the useIntersectionObserver
|
49
68
|
});
|
50
69
|
};
|
51
70
|
|
@@ -55,86 +74,164 @@ const getURLParams = (hash) => {
|
|
55
74
|
return { target, yOffsetFromURLParams: yOffset };
|
56
75
|
};
|
57
76
|
|
77
|
+
const getTarget = (hash) => {
|
78
|
+
// Try to get target from URL params
|
79
|
+
const { target, yOffsetFromURLParams } = getURLParams(hash);
|
80
|
+
if (!target) return { targetElement: null, yOffsetFromURLParams: null };
|
81
|
+
|
82
|
+
// Look for target element in DOM
|
83
|
+
const targetElement = getTargetElementHelper(target);
|
84
|
+
if (!targetElement) {
|
85
|
+
return { targetElement: null, yOffsetFromURLParams: null };
|
86
|
+
}
|
87
|
+
|
88
|
+
return { targetElement, yOffsetFromURLParams };
|
89
|
+
};
|
90
|
+
|
91
|
+
const handleScroll = ({ hash, observerRef, setCurrentTarget, ...options }) => {
|
92
|
+
const { targetElement, yOffsetFromURLParams } = getTarget(hash);
|
93
|
+
|
94
|
+
/**
|
95
|
+
* If no targetElement found, we need to navigate using the hash
|
96
|
+
* because the targetElement is on another page.
|
97
|
+
*/
|
98
|
+
if (!targetElement) {
|
99
|
+
window.location.href = hash;
|
100
|
+
return;
|
101
|
+
}
|
102
|
+
|
103
|
+
const targetObj = {
|
104
|
+
targetElement,
|
105
|
+
...(yOffsetFromURLParams ? { yOffset: parseInt(yOffsetFromURLParams, 10) } : {}),
|
106
|
+
hash, // Add hash to the target object so onIntersect can update the URL
|
107
|
+
...options,
|
108
|
+
};
|
109
|
+
|
110
|
+
// Trigger scrollToTriggered callback if it exists
|
111
|
+
if (typeof options.scrollToTriggered === 'function') {
|
112
|
+
options.scrollToTriggered();
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Check if we need to scroll up the page. If we need to
|
117
|
+
* scroll up, chances are the rest of the the page content
|
118
|
+
* is loaded so we don't need to automatically scroll down the
|
119
|
+
* page and the targetElementPositionTop should be accurate enough.
|
120
|
+
*/
|
121
|
+
const targetElementPositionTop = targetElement.getBoundingClientRect().top;
|
122
|
+
if (
|
123
|
+
targetElementPositionTop + window.scrollY <
|
124
|
+
window.scrollY - window.innerHeight
|
125
|
+
) {
|
126
|
+
targetObj.scrollAmount = targetElementPositionTop + window.scrollY;
|
127
|
+
scrollToTarget(targetObj);
|
128
|
+
setCurrentTarget(targetObj);
|
129
|
+
return;
|
130
|
+
}
|
131
|
+
|
132
|
+
// AUTO scroll down the page
|
133
|
+
scrollToBottom();
|
134
|
+
const observer = createMutationObserver((mutationsList, observer) => {
|
135
|
+
scrollToBottom();
|
136
|
+
});
|
137
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
138
|
+
|
139
|
+
// Store observer in ref so it can be disconnected later
|
140
|
+
if (observerRef) {
|
141
|
+
observerRef.current = observer;
|
142
|
+
}
|
143
|
+
|
144
|
+
// Set current target so the intersectionObserver can use it
|
145
|
+
setCurrentTarget(targetObj);
|
146
|
+
};
|
58
147
|
|
59
148
|
/**
|
60
|
-
*
|
61
|
-
*
|
62
|
-
*
|
63
|
-
*
|
149
|
+
* useScrollTo
|
150
|
+
*
|
151
|
+
* A hook for handling smooth scrolling to elements on pages with lazy-loaded content.
|
152
|
+
* It can auto-scroll based on the URL hash on page load, or when an element with a specific
|
153
|
+
* href (e.g. `#/scroll-to?target=158827`) is clicked. The hook accounts for dynamic content
|
154
|
+
* loading that may shift the target element's position during scrolling.
|
64
155
|
*
|
65
|
-
*
|
66
|
-
*
|
67
|
-
* -
|
68
|
-
*
|
69
|
-
* - Resolves layout shift issues by re-calculating the target position using an intersection observer.
|
70
|
-
* - Automatically attaches click event listeners to links with href="#scroll-to?target=",
|
71
|
-
* target can either be a element id, class name or chunk id.
|
72
|
-
* - Handles URL navigation with scroll-to hash parameters.
|
156
|
+
* The hook determines the initial position of the target element. If the element is above the
|
157
|
+
* current scroll position, it assumes the page is loaded and scrolls directly to the element.
|
158
|
+
* If the element is below, it auto-scrolls to the bottom of the page, then corrects the scroll
|
159
|
+
* position once the target is in view, ensuring the element is aligned at the top.
|
73
160
|
*
|
74
|
-
*
|
75
|
-
*
|
76
|
-
*
|
161
|
+
* Additional features:
|
162
|
+
* - Handles scroll offsets (static or dynamic).
|
163
|
+
* - Allows for a theme-specific offset (e.g., to account for navigation bar heights) via `additionalYOffset`.
|
164
|
+
* - Updates the URL hash after scrolling.
|
165
|
+
* - Supports callbacks for scroll start and completion.
|
166
|
+
* - Cleans up observers and event listeners automatically.
|
167
|
+
*
|
168
|
+
* @param {Object} [options] - Optional configuration object.
|
169
|
+
* @param {number|function} [options.yOffset] - A fixed offset (in px) or a function returning an offset, applied when scrolling to the target. Positive values scroll up from the target, negative values scroll down from the target.
|
170
|
+
* @param {number} [options.additionalYOffset] - An extra offset (in px) to account for theme-specific adjustments, such as navigation bar heights.
|
171
|
+
* @param {function} [options.scrollToTriggered] - Callback fired when a scroll-to event is triggered.
|
172
|
+
* @param {function} [options.scrollToComplete] - Callback fired when scrolling to the target is complete.
|
173
|
+
*
|
174
|
+
* @returns {void}
|
77
175
|
*/
|
78
|
-
const useScrollTo = (options) => {
|
176
|
+
const useScrollTo = (options = {}) => {
|
79
177
|
const [currentTarget, setCurrentTarget] = useState(null);
|
80
|
-
const
|
81
|
-
|
82
|
-
const handleScrollTo = (hash) => {
|
83
|
-
const { target, yOffsetFromURLParams } = getURLParams(hash || window.location.hash);
|
84
|
-
if (!target) return;
|
85
|
-
|
86
|
-
const targetElement = getTargetElementHelper(target);
|
87
|
-
const targetObj = {
|
88
|
-
targetElement,
|
89
|
-
additionalYOffset: yOffsetFromURLParams,
|
90
|
-
...(hash && { hash }), // Add hash to the target object so onIntersect can update the URL
|
91
|
-
...options,
|
92
|
-
}
|
178
|
+
const observerRef = useRef(null);
|
93
179
|
|
94
|
-
|
95
|
-
|
180
|
+
useIntersectionObserver({
|
181
|
+
target: currentTarget?.targetElement,
|
182
|
+
onIntersect: () => {
|
183
|
+
// This will fire on first scroll
|
184
|
+
if (observerRef.current) {
|
185
|
+
// Kill observer that is handling the scrollToBottom;
|
186
|
+
observerRef.current.disconnect();
|
187
|
+
observerRef.current = null;
|
96
188
|
|
97
|
-
|
98
|
-
|
99
|
-
|
189
|
+
/**
|
190
|
+
* Kill current scroll
|
191
|
+
* (only way to do it because we can't currently
|
192
|
+
* attach an abortcontroller to scrollTo)
|
193
|
+
*/
|
194
|
+
window.scrollTo({
|
195
|
+
top: window.scrollY,
|
196
|
+
left: 0,
|
197
|
+
behavior: 'auto',
|
198
|
+
});
|
100
199
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
* stopping the scroll. This is done by using the AbortController.
|
118
|
-
* Then it scrollToTarget again, this in turn gets the most up to date
|
119
|
-
* getBoundingClientRect() value and scrolls to the target element.
|
120
|
-
*
|
121
|
-
*/
|
122
|
-
useIntersectionObserver({
|
123
|
-
target: currentTarget?.targetElement,
|
124
|
-
onIntersect: () => {
|
125
|
-
if (scrollAbortControllerRef.current) {
|
126
|
-
scrollAbortControllerRef.current.abort(); // Stop the scroll
|
127
|
-
scrollAbortControllerRef.current = null; // Reset the controller
|
128
|
-
scrollToTarget({ ...currentTarget }); // Fire the scrollToTarget function again
|
129
|
-
if (currentTarget?.hash) window.location.hash = currentTarget.hash; // Update the URL hash
|
200
|
+
// Get targets current position because it might have changed
|
201
|
+
const targetElementPositionTop =
|
202
|
+
currentTarget.targetElement.getBoundingClientRect().top;
|
203
|
+
|
204
|
+
/**
|
205
|
+
* The scrollAmount is the current scroll position, which
|
206
|
+
* will be near bottom of the page. Plus the current scroll
|
207
|
+
* position, which is at the top of the page.
|
208
|
+
*/
|
209
|
+
const scrollAmount = targetElementPositionTop + window.scrollY;
|
210
|
+
|
211
|
+
// Update current target state (will need this to trigger the scroll correction)
|
212
|
+
setCurrentTarget({
|
213
|
+
...currentTarget,
|
214
|
+
scrollAmount,
|
215
|
+
});
|
130
216
|
}
|
131
|
-
|
217
|
+
|
218
|
+
// This will fire after the scroll correction
|
219
|
+
if (currentTarget?.scrollCorrectionFired) {
|
220
|
+
|
221
|
+
// Trigger scrollToComplete callback if it exists
|
222
|
+
if (typeof currentTarget.scrollToComplete === 'function') {
|
223
|
+
currentTarget.scrollToComplete();
|
224
|
+
}
|
225
|
+
|
226
|
+
// Remove current target
|
227
|
+
setCurrentTarget(null);
|
228
|
+
}
|
229
|
+
},
|
132
230
|
});
|
133
231
|
|
134
|
-
|
135
|
-
// Handle onclick scroll to events
|
232
|
+
// Handles onclick scroll to events
|
136
233
|
useEffect(() => {
|
137
|
-
const clickEventController = new AbortController()
|
234
|
+
const clickEventController = new AbortController();
|
138
235
|
const scrollToEls = document.querySelectorAll(
|
139
236
|
'[href*="#scroll-to"], [href*="#/scroll-to"]'
|
140
237
|
);
|
@@ -143,29 +240,80 @@ const useScrollTo = (options) => {
|
|
143
240
|
el.addEventListener(
|
144
241
|
'click',
|
145
242
|
(e) => {
|
146
|
-
|
147
|
-
/**
|
148
|
-
* Prevent default behavior, hash change is handled onIntersect,
|
149
|
-
* otherwise it will cause the scroll to be instant
|
150
|
-
*/
|
151
243
|
e.preventDefault();
|
152
|
-
|
244
|
+
const hash = e.currentTarget.getAttribute('href');
|
245
|
+
if (hash) {
|
246
|
+
handleScroll({
|
247
|
+
hash: hash,
|
248
|
+
observerRef,
|
249
|
+
setCurrentTarget,
|
250
|
+
...options,
|
251
|
+
});
|
252
|
+
}
|
153
253
|
},
|
154
254
|
{ signal: clickEventController.signal }
|
155
255
|
);
|
156
256
|
});
|
157
257
|
|
158
|
-
//On click element event listeners cleanup
|
159
258
|
return () => {
|
160
|
-
clickEventController.abort()
|
161
|
-
}
|
259
|
+
clickEventController.abort();
|
260
|
+
};
|
162
261
|
}, []);
|
163
262
|
|
164
|
-
//
|
263
|
+
// Handles when a user navigates to a URL with a scroll to hash
|
165
264
|
useEffect(() => {
|
166
|
-
|
167
|
-
|
265
|
+
const timeout = setTimeout(() => {
|
266
|
+
// Start scrolling
|
267
|
+
handleScroll({
|
268
|
+
hash: window.location.hash,
|
269
|
+
observerRef,
|
270
|
+
setCurrentTarget,
|
271
|
+
...options,
|
272
|
+
});
|
273
|
+
}, 1000);
|
274
|
+
|
275
|
+
/**
|
276
|
+
* Clears observer after 5 seconds if whatever reason
|
277
|
+
* observerRef.current.disconnect() is never fired. Otherwise
|
278
|
+
* scrollToBottom will constantly fire if the scroll gets
|
279
|
+
* to the bottom of the page.
|
280
|
+
*/
|
281
|
+
if (observerRef && observerRef.current) {
|
282
|
+
// Clear any previous timeout
|
283
|
+
if (observerRef.disconnectTimeout) {
|
284
|
+
clearTimeout(observerRef.disconnectTimeout);
|
285
|
+
}
|
286
|
+
observerRef.disconnectTimeout = setTimeout(() => {
|
287
|
+
if (observerRef.current) {
|
288
|
+
observerRef.current.disconnect();
|
289
|
+
observerRef.current = null;
|
290
|
+
}
|
291
|
+
}, 5000);
|
292
|
+
}
|
293
|
+
|
294
|
+
return () => {
|
295
|
+
clearTimeout(timeout);
|
296
|
+
// Disconnect observer on cleanup
|
297
|
+
if (observerRef.current) {
|
298
|
+
observerRef.current.disconnect();
|
299
|
+
}
|
300
|
+
if (observerRef.disconnectTimeout) {
|
301
|
+
clearTimeout(observerRef.disconnectTimeout);
|
302
|
+
observerRef.disconnectTimeout = null;
|
303
|
+
}
|
304
|
+
};
|
305
|
+
}, []);
|
168
306
|
|
307
|
+
// Handles scroll correction
|
308
|
+
useEffect(() => {
|
309
|
+
if (currentTarget?.scrollAmount && !currentTarget?.scrollCorrectionFired) {
|
310
|
+
scrollToTarget(currentTarget);
|
311
|
+
setCurrentTarget({
|
312
|
+
...currentTarget,
|
313
|
+
scrollCorrectionFired: true,
|
314
|
+
});
|
315
|
+
}
|
316
|
+
}, [currentTarget]);
|
169
317
|
};
|
170
318
|
|
171
319
|
export default useScrollTo;
|
package/package.json
CHANGED
@@ -18,6 +18,7 @@ const SaveSearchButton = (props) => {
|
|
18
18
|
removeSavedSearchAsync,
|
19
19
|
style,
|
20
20
|
notificationMessage,
|
21
|
+
renderAsButton,
|
21
22
|
showNotification,
|
22
23
|
} = props;
|
23
24
|
|
@@ -42,6 +43,19 @@ const SaveSearchButton = (props) => {
|
|
42
43
|
}
|
43
44
|
};
|
44
45
|
|
46
|
+
if (renderAsButton) return (
|
47
|
+
<button
|
48
|
+
role="button"
|
49
|
+
onClick={toggleSearch}
|
50
|
+
className={`${className} ${savedSearch ? 'saved' : ''}`}
|
51
|
+
{...(style && {
|
52
|
+
style,
|
53
|
+
})}
|
54
|
+
>
|
55
|
+
{savedSearch ? SavedComponent : UnsavedComponent}
|
56
|
+
</button>
|
57
|
+
)
|
58
|
+
|
45
59
|
return (
|
46
60
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid
|
47
61
|
<a
|
@@ -56,6 +70,7 @@ const SaveSearchButton = (props) => {
|
|
56
70
|
|
57
71
|
SaveSearchButton.propTypes = {
|
58
72
|
search: PropTypes.object.isRequired,
|
73
|
+
renderAsButton: PropTypes.bool,
|
59
74
|
className: PropTypes.string,
|
60
75
|
savedSearches: PropTypes.array,
|
61
76
|
UnsavedComponent: PropTypes.element.isRequired,
|
@@ -79,6 +94,7 @@ SaveSearchButton.defaultProps = {
|
|
79
94
|
className: '',
|
80
95
|
style: {},
|
81
96
|
showNotification: false,
|
97
|
+
renderAsButton: false,
|
82
98
|
};
|
83
99
|
|
84
100
|
const mapStateToProps = (state) => ({
|
package/utils/index.js
CHANGED
@@ -157,3 +157,20 @@ export function JSON_to_URLEncoded(element,key,list){
|
|
157
157
|
export function isFunction(functionToCheck) {
|
158
158
|
return functionToCheck && Object.prototype.toString.call(functionToCheck) === '[object Function]';
|
159
159
|
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Creates a MutationObserver that invokes the provided callback function
|
163
|
+
* whenever mutations are observed. The callback receives the list of mutations
|
164
|
+
* and the observer instance as arguments.
|
165
|
+
*
|
166
|
+
* @param {Function} callback - Function to be called when mutations are observed.
|
167
|
+
* Receives (mutationsList: MutationRecord[], observer: MutationObserver).
|
168
|
+
* @returns {MutationObserver} The created MutationObserver instance.
|
169
|
+
*/
|
170
|
+
export function createMutationObserver(callback) {
|
171
|
+
return new MutationObserver((mutationsList, observer) => {
|
172
|
+
if (typeof callback === 'function') {
|
173
|
+
callback(mutationsList, observer);
|
174
|
+
}
|
175
|
+
});
|
176
|
+
};
|