@splendidlabz/utils 1.8.1 → 1.9.0
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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/dom/events.cjs +0 -36
- package/dist/cjs/dom/index.cjs +90 -36
- package/dist/cjs/dom/observers/index.cjs +92 -2
- package/dist/cjs/dom/observers/scroll-observer.cjs +170 -0
- package/dist/cjs/lib/functions/callback.cjs +31 -0
- package/dist/cjs/lib/functions/functional.cjs +20 -0
- package/dist/cjs/lib/functions/index.cjs +20 -0
- package/dist/cjs/lib/index.cjs +20 -0
- package/dist/esm/dom/events.js +0 -35
- package/dist/esm/dom/index.js +89 -35
- package/dist/esm/dom/observers/index.js +90 -1
- package/dist/esm/dom/observers/scroll-observer.js +144 -0
- package/dist/esm/lib/functions/callback.js +7 -0
- package/dist/esm/lib/functions/functional.js +18 -0
- package/dist/esm/lib/functions/index.js +18 -0
- package/dist/esm/lib/index.js +18 -0
- package/dist/types/dom/events.d.cts +90 -18
- package/dist/types/dom/index.d.cts +2 -1
- package/dist/types/dom/observers/index.d.cts +1 -0
- package/dist/types/dom/observers/scroll-observer.d.cts +27 -0
- package/dist/types/lib/functions/callback.d.cts +8 -0
- package/dist/types/lib/functions/functional.d.cts +71 -5
- package/dist/types/lib/functions/index.d.cts +1 -1
- package/dist/types/lib/index.d.cts +1 -1
- package/package.json +1 -1
- package/src/dom/events.js +44 -50
- package/src/dom/observers/index.js +1 -0
- package/src/dom/observers/resize-observer.js +5 -5
- package/src/dom/observers/scroll-observer.js +132 -0
- package/src/lib/functions/callback.js +8 -0
- package/src/lib/functions/functional.js +82 -0
- package/src/lib/functions/functional.test.js +196 -0
|
@@ -1,20 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} EventListener
|
|
3
|
+
* @property {Element} node - The DOM element to attach the event listener to
|
|
4
|
+
* @property {string} event - The event type (e.g., 'click', 'keydown')
|
|
5
|
+
* @property {Function} handler - The event handler function
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ListenerManager
|
|
9
|
+
* @property {EventListener[]} list - Array of current event listeners
|
|
10
|
+
* @property {function(EventListener): void} add - Add a new event listener
|
|
11
|
+
* @property {function(EventListener): void} remove - Remove an event listener
|
|
12
|
+
* @property {function(): void} clear - Remove all event listeners
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} CustomEventOptions
|
|
16
|
+
* @property {boolean} [bubbles=false] - Whether the event bubbles up through the DOM
|
|
17
|
+
* @property {boolean} [cancelable=false] - Whether the event can be canceled
|
|
18
|
+
* @property {boolean} [composed=false] - Whether the event will trigger listeners outside of a shadow root
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Adds multiple event listeners to their respective DOM elements
|
|
22
|
+
* @param {EventListener[]} listeners - Array of listener objects
|
|
23
|
+
*/
|
|
24
|
+
declare function addListeners(listeners: EventListener[]): void;
|
|
25
|
+
/**
|
|
26
|
+
* Removes multiple event listeners from their respective DOM elements
|
|
27
|
+
* @param {EventListener[]} listeners - Array of listener objects
|
|
28
|
+
*/
|
|
29
|
+
declare function removeListeners(listeners: EventListener[]): void;
|
|
30
|
+
/**
|
|
31
|
+
* Dispatches a custom event from a DOM node
|
|
32
|
+
* @param {Element} node - The DOM element to dispatch the event from
|
|
33
|
+
* @param {string} eventName - The name of the custom event
|
|
34
|
+
* @param {*} detail - The detail data to include with the event
|
|
35
|
+
* @param {CustomEventOptions} [options={}] - Additional event options
|
|
36
|
+
*/
|
|
37
|
+
declare function dispatchEvent(node: Element, eventName: string, detail: any, options?: CustomEventOptions): void;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new CustomEvent with updated properties from an existing event
|
|
40
|
+
* @param {Event} event - The original event object
|
|
41
|
+
* @param {Object} updates - Properties to update in the new event
|
|
42
|
+
* @return {CustomEvent} A new CustomEvent with updated properties
|
|
43
|
+
*/
|
|
44
|
+
declare function updateEvent(event: Event, updates: any): CustomEvent;
|
|
45
|
+
type EventListener = {
|
|
46
|
+
/**
|
|
47
|
+
* - The DOM element to attach the event listener to
|
|
48
|
+
*/
|
|
49
|
+
node: Element;
|
|
50
|
+
/**
|
|
51
|
+
* - The event type (e.g., 'click', 'keydown')
|
|
52
|
+
*/
|
|
53
|
+
event: string;
|
|
54
|
+
/**
|
|
55
|
+
* - The event handler function
|
|
56
|
+
*/
|
|
57
|
+
handler: Function;
|
|
58
|
+
};
|
|
59
|
+
type ListenerManager = {
|
|
60
|
+
/**
|
|
61
|
+
* - Array of current event listeners
|
|
62
|
+
*/
|
|
63
|
+
list: EventListener[];
|
|
64
|
+
/**
|
|
65
|
+
* - Add a new event listener
|
|
66
|
+
*/
|
|
67
|
+
add: (arg0: EventListener) => void;
|
|
68
|
+
/**
|
|
69
|
+
* - Remove an event listener
|
|
70
|
+
*/
|
|
71
|
+
remove: (arg0: EventListener) => void;
|
|
72
|
+
/**
|
|
73
|
+
* - Remove all event listeners
|
|
74
|
+
*/
|
|
75
|
+
clear: () => void;
|
|
76
|
+
};
|
|
77
|
+
type CustomEventOptions = {
|
|
78
|
+
/**
|
|
79
|
+
* - Whether the event bubbles up through the DOM
|
|
80
|
+
*/
|
|
81
|
+
bubbles?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* - Whether the event can be canceled
|
|
84
|
+
*/
|
|
85
|
+
cancelable?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* - Whether the event will trigger listeners outside of a shadow root
|
|
88
|
+
*/
|
|
89
|
+
composed?: boolean;
|
|
18
90
|
};
|
|
19
91
|
|
|
20
|
-
export {
|
|
92
|
+
export { type CustomEventOptions, type EventListener, type ListenerManager, addListeners, dispatchEvent, removeListeners, updateEvent };
|
|
@@ -6,7 +6,7 @@ export { boundingBox, boundingBoxRelativeToAncestor } from './bounding-box.cjs';
|
|
|
6
6
|
export { copyRichText } from './clipboard.cjs';
|
|
7
7
|
export { cookies, getCookie } from './cookie.cjs';
|
|
8
8
|
export { getCSSValue, getCSSVar, setCSSValue, setCSSVar } from './css-vars.cjs';
|
|
9
|
-
export {
|
|
9
|
+
export { CustomEventOptions, EventListener, ListenerManager, addListeners, dispatchEvent, removeListeners, updateEvent } from './events.cjs';
|
|
10
10
|
export { Focusable, Focusables, getFocusableElements } from './focusable.cjs';
|
|
11
11
|
export { em, getUnit, lh, rem, toPx } from './font-size.cjs';
|
|
12
12
|
export { getAncestorWithSiblings, getChildrenElements, getElement, getNodeType, getParentElement, getSelfIndex, getSiblingElements, isAncestor } from './get-element.cjs';
|
|
@@ -17,6 +17,7 @@ export { imagesLoaded, mediaLoaded, videosLoaded } from './media.cjs';
|
|
|
17
17
|
export { intersectionObserver } from './observers/intersection-observer.cjs';
|
|
18
18
|
export { mutationObserver } from './observers/mutation-observer.cjs';
|
|
19
19
|
export { resizeObserver } from './observers/resize-observer.cjs';
|
|
20
|
+
export { scrollObserver } from './observers/scroll-observer.cjs';
|
|
20
21
|
export { PKCE } from './pkce.cjs';
|
|
21
22
|
export { queryParams } from './query-params.cjs';
|
|
22
23
|
export { randomString, uuid } from './random-string.cjs';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Observer - Optimized for performance
|
|
3
|
+
* @param {Element} node - The element to observe scroll events on
|
|
4
|
+
* @param {Object} options - Configuration options
|
|
5
|
+
* @param {Number} options.threshold - Float between 0 to 1. When to trigger callback (0.75 = 75% down page)
|
|
6
|
+
* @param {Number} options.tolerance - Float between 0 to 1. Tolerance zone around threshold
|
|
7
|
+
* @param {Number} options.throttle - Throttle interval in ms (default: 16ms for ~60fps)
|
|
8
|
+
* @param {Boolean} options.once - Only fire threshold callback once (default: false)
|
|
9
|
+
* @param {Function} options.callback - Called on every scroll with scrollPercent
|
|
10
|
+
* @param {Function} options.onScrollDown - Called when scrolling down
|
|
11
|
+
* @param {Function} options.onScrollUp - Called when scrolling up
|
|
12
|
+
* @param {Function} options.onEnterThreshold - Called when entering threshold zone
|
|
13
|
+
*/
|
|
14
|
+
declare function scrollObserver(node: Element, options?: {
|
|
15
|
+
threshold: number;
|
|
16
|
+
tolerance: number;
|
|
17
|
+
throttle: number;
|
|
18
|
+
once: boolean;
|
|
19
|
+
callback: Function;
|
|
20
|
+
onScrollDown: Function;
|
|
21
|
+
onScrollUp: Function;
|
|
22
|
+
onEnterThreshold: Function;
|
|
23
|
+
}): {
|
|
24
|
+
destroy(): void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { scrollObserver };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely executes a callback function if it's a function
|
|
3
|
+
* @param {Function|*} fn - The potential callback function
|
|
4
|
+
* @param {...*} data - Data arguments to pass to the callback
|
|
5
|
+
*/
|
|
6
|
+
declare function runCallback(fn: Function | any, ...data: any[]): void;
|
|
7
|
+
|
|
8
|
+
export { runCallback };
|
|
@@ -1,6 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Creates a curried version of a function that can be called with partial arguments
|
|
3
|
+
* @param {Function} fn - The function to curry
|
|
4
|
+
* @return {Function} The curried function
|
|
5
|
+
* @property {any} return - The result of calling the original function when all arguments are provided
|
|
6
|
+
* @example
|
|
7
|
+
* const add = (a, b, c) => a + b + c
|
|
8
|
+
* const curriedAdd = curry(add)
|
|
9
|
+
* curriedAdd(1)(2)(3) // 6
|
|
10
|
+
* curriedAdd(1, 2)(3) // 6
|
|
11
|
+
* curriedAdd(1)(2, 3) // 6
|
|
12
|
+
*/
|
|
13
|
+
declare function curry(fn: Function): Function;
|
|
14
|
+
/**
|
|
15
|
+
* Composes functions from right to left (synchronous)
|
|
16
|
+
* @param {...Function} fns - Functions to compose
|
|
17
|
+
* @return {Function} The composed function
|
|
18
|
+
* @property {any} return - The result of applying all functions in sequence
|
|
19
|
+
* @example
|
|
20
|
+
* const add1 = x => x + 1
|
|
21
|
+
* const multiply2 = x => x * 2
|
|
22
|
+
* const composed = compose(add1, multiply2)
|
|
23
|
+
* composed(3) // 7 (3 * 2 + 1)
|
|
24
|
+
*/
|
|
25
|
+
declare function compose(...fns: Function[]): Function;
|
|
26
|
+
/**
|
|
27
|
+
* Composes functions from right to left (asynchronous)
|
|
28
|
+
* @param {...Function} fns - Functions to compose (can be sync or async)
|
|
29
|
+
* @return {Function} The composed async function
|
|
30
|
+
* @property {Promise<any>} return - A Promise that resolves to the result of applying all functions
|
|
31
|
+
* @example
|
|
32
|
+
* const add1 = x => x + 1
|
|
33
|
+
* const multiplyAsync = async x => x * 2
|
|
34
|
+
* await composeAsync(add1, multiplyAsync)(3) // 7 (3 * 2 + 1)
|
|
35
|
+
*/
|
|
36
|
+
declare function composeAsync(...fns: Function[]): Function;
|
|
37
|
+
/**
|
|
38
|
+
* Pipes functions from left to right (synchronous)
|
|
39
|
+
* @param {...Function} fns - Functions to pipe
|
|
40
|
+
* @return {Function} The piped function
|
|
41
|
+
* @property {any} return - The result of applying all functions in sequence
|
|
42
|
+
* @example
|
|
43
|
+
* const add1 = x => x + 1
|
|
44
|
+
* const multiply2 = x => x * 2
|
|
45
|
+
* const piped = pipe(add1, multiply2)
|
|
46
|
+
* piped(3) // 8 (3 + 1) * 2
|
|
47
|
+
*/
|
|
48
|
+
declare function pipe(...fns: Function[]): Function;
|
|
49
|
+
/**
|
|
50
|
+
* Pipes functions from left to right (asynchronous)
|
|
51
|
+
* @param {...Function} fns - Functions to pipe (can be sync or async)
|
|
52
|
+
* @return {Function} The piped async function
|
|
53
|
+
* @property {Promise<any>} return - A Promise that resolves to the result of applying all functions
|
|
54
|
+
* @example
|
|
55
|
+
* const add1 = x => x + 1
|
|
56
|
+
* const multiplyAsync = async x => x * 2
|
|
57
|
+
* await pipeAsync(add1, multiplyAsync)(3) // 8 ((3 + 1) * 2)
|
|
58
|
+
*/
|
|
59
|
+
declare function pipeAsync(...fns: Function[]): Function;
|
|
60
|
+
/**
|
|
61
|
+
* Calls a function n times with the current index and returns an array of results
|
|
62
|
+
* @param {Function} fn - Function to call (receives index as argument)
|
|
63
|
+
* @param {number} n - Number of times to call the function
|
|
64
|
+
* @return {any[]} Array of results from calling the function
|
|
65
|
+
* @property {any[]} return - Array containing the results of each function call
|
|
66
|
+
* @example
|
|
67
|
+
* times(i => i * 2, 3) // [0, 2, 4]
|
|
68
|
+
* times(() => Math.random(), 2) // [0.123, 0.456] (random values)
|
|
69
|
+
*/
|
|
70
|
+
declare function times(fn: Function, n: number): any[];
|
|
5
71
|
|
|
6
|
-
export { compose, curry, pipe, times };
|
|
72
|
+
export { compose, composeAsync, curry, pipe, pipeAsync, times };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { debounce } from './debounce.cjs';
|
|
2
2
|
export { getEnv } from './env.cjs';
|
|
3
|
-
export { compose, curry, pipe, times } from './functional.cjs';
|
|
3
|
+
export { compose, composeAsync, curry, pipe, pipeAsync, times } from './functional.cjs';
|
|
4
4
|
export { throttle } from './throttle.cjs';
|
|
5
5
|
export { delay, timeout, wait } from './timeout.cjs';
|
|
@@ -13,7 +13,7 @@ export { flattenArrayFields, formDataToObject } from './form/form-data.cjs';
|
|
|
13
13
|
export { SanitizeOptions, sanitize, sanitizeArray, sanitizeObject } from './form/sanitize.cjs';
|
|
14
14
|
export { debounce } from './functions/debounce.cjs';
|
|
15
15
|
export { getEnv } from './functions/env.cjs';
|
|
16
|
-
export { compose, curry, pipe, times } from './functions/functional.cjs';
|
|
16
|
+
export { compose, composeAsync, curry, pipe, pipeAsync, times } from './functions/functional.cjs';
|
|
17
17
|
export { throttle } from './functions/throttle.cjs';
|
|
18
18
|
export { delay, timeout, wait } from './functions/timeout.cjs';
|
|
19
19
|
export { isHashedValue } from './hash.cjs';
|
package/package.json
CHANGED
package/src/dom/events.js
CHANGED
|
@@ -1,26 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} EventListener
|
|
3
|
+
* @property {Element} node - The DOM element to attach the event listener to
|
|
4
|
+
* @property {string} event - The event type (e.g., 'click', 'keydown')
|
|
5
|
+
* @property {Function} handler - The event handler function
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} ListenerManager
|
|
10
|
+
* @property {EventListener[]} list - Array of current event listeners
|
|
11
|
+
* @property {function(EventListener): void} add - Add a new event listener
|
|
12
|
+
* @property {function(EventListener): void} remove - Remove an event listener
|
|
13
|
+
* @property {function(): void} clear - Remove all event listeners
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} CustomEventOptions
|
|
18
|
+
* @property {boolean} [bubbles=false] - Whether the event bubbles up through the DOM
|
|
19
|
+
* @property {boolean} [cancelable=false] - Whether the event can be canceled
|
|
20
|
+
* @property {boolean} [composed=false] - Whether the event will trigger listeners outside of a shadow root
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Adds multiple event listeners to their respective DOM elements
|
|
25
|
+
* @param {EventListener[]} listeners - Array of listener objects
|
|
26
|
+
*/
|
|
1
27
|
export function addListeners(listeners) {
|
|
2
28
|
listeners.forEach(({ node, event, handler }) => {
|
|
3
29
|
node.addEventListener(event, handler)
|
|
4
30
|
})
|
|
5
31
|
}
|
|
6
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Removes multiple event listeners from their respective DOM elements
|
|
35
|
+
* @param {EventListener[]} listeners - Array of listener objects
|
|
36
|
+
*/
|
|
7
37
|
export function removeListeners(listeners) {
|
|
8
38
|
listeners.forEach(({ node, event, handler }) => {
|
|
9
39
|
node.removeEventListener(event, handler)
|
|
10
40
|
})
|
|
11
41
|
}
|
|
12
42
|
|
|
13
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Dispatches a custom event from a DOM node
|
|
45
|
+
* @param {Element} node - The DOM element to dispatch the event from
|
|
46
|
+
* @param {string} eventName - The name of the custom event
|
|
47
|
+
* @param {*} detail - The detail data to include with the event
|
|
48
|
+
* @param {CustomEventOptions} [options={}] - Additional event options
|
|
49
|
+
*/
|
|
14
50
|
export function dispatchEvent(node, eventName, detail, options = {}) {
|
|
15
51
|
node.dispatchEvent(
|
|
16
52
|
new CustomEvent(eventName, {
|
|
17
53
|
...options,
|
|
18
54
|
detail,
|
|
19
|
-
})
|
|
55
|
+
}),
|
|
20
56
|
)
|
|
21
57
|
}
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Creates a new CustomEvent with updated properties from an existing event
|
|
61
|
+
* @param {Event} event - The original event object
|
|
62
|
+
* @param {Object} updates - Properties to update in the new event
|
|
63
|
+
* @return {CustomEvent} A new CustomEvent with updated properties
|
|
64
|
+
*/
|
|
24
65
|
export function updateEvent(event, updates) {
|
|
25
66
|
const { type, ...rest } = event
|
|
26
67
|
return new CustomEvent(event.type, {
|
|
@@ -28,50 +69,3 @@ export function updateEvent(event, updates) {
|
|
|
28
69
|
...updates,
|
|
29
70
|
})
|
|
30
71
|
}
|
|
31
|
-
|
|
32
|
-
// Doesn't seem to be used anywhere now.
|
|
33
|
-
// Switch to addListeners and removeListeners instead
|
|
34
|
-
export function createListeners(listeners) {
|
|
35
|
-
listeners = listeners || []
|
|
36
|
-
|
|
37
|
-
listeners.forEach(listener => {
|
|
38
|
-
const { node, event, handler } = listener
|
|
39
|
-
node.addEventListener(event, handler)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
get list() {
|
|
44
|
-
return listeners
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
add({ node, event, handler }) {
|
|
48
|
-
listeners.push({ node, event, handler })
|
|
49
|
-
node.addEventListener(event, handler)
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
remove({ node, event, handler }) {
|
|
53
|
-
const index = listeners.findIndex(listener => {
|
|
54
|
-
if (
|
|
55
|
-
listener.node === node &&
|
|
56
|
-
listener.event === event &&
|
|
57
|
-
listener.handler === handler
|
|
58
|
-
) {
|
|
59
|
-
return true
|
|
60
|
-
}
|
|
61
|
-
return false
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
if (index !== -1) {
|
|
65
|
-
listeners.splice(index, 1)
|
|
66
|
-
node.removeEventListener(event, handler)
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
clear() {
|
|
71
|
-
listeners.forEach(({ node, event, handler }) => {
|
|
72
|
-
listeners = []
|
|
73
|
-
node.removeEventListener(event, handler)
|
|
74
|
-
})
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -4,8 +4,8 @@ import { useObserverMethodOnTarget } from './observer.js'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Creates and manages a ResizeObserver instance to monitor size changes of a target element.
|
|
7
|
-
*
|
|
8
|
-
* @param {Element|Window|NodeList|Element[]} target - The element(s) to observe.
|
|
7
|
+
*
|
|
8
|
+
* @param {Element|Window|NodeList|Element[]} target - The element(s) to observe.
|
|
9
9
|
* - If window is provided, document.body will be observed instead.
|
|
10
10
|
* - If NodeList or Array of elements is provided, all elements will be observed.
|
|
11
11
|
* @param {Object} options - Configuration options for the resize observer
|
|
@@ -13,13 +13,13 @@ import { useObserverMethodOnTarget } from './observer.js'
|
|
|
13
13
|
* @param {Function} [options.callback] - Optional callback function that will be called when resize changes are detected.
|
|
14
14
|
* If not provided, a 'resize-obs' event will be dispatched on the target.
|
|
15
15
|
* @param {Object} [options.observerOptions] - Additional options to pass to ResizeObserver.observe()
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @returns {Object} An object with methods to control the observer:
|
|
18
18
|
* - observe(target, options): Start observing a new target element
|
|
19
19
|
* - unobserve(target): Stop observing a target element
|
|
20
20
|
* - disconnect(): Disconnect the observer and stop all observations
|
|
21
21
|
* - destroy(): Alias for disconnect()
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* @example
|
|
24
24
|
* // Basic usage with callback
|
|
25
25
|
* resizeObserver(element, {
|
|
@@ -27,7 +27,7 @@ import { useObserverMethodOnTarget } from './observer.js'
|
|
|
27
27
|
* console.log('Element resized:', entry.contentRect);
|
|
28
28
|
* }
|
|
29
29
|
* });
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
31
|
* @example
|
|
32
32
|
* // Usage with event listener
|
|
33
33
|
* resizeObserver(element);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* eslint-env browser */
|
|
2
|
+
import { resizeObserver } from './resize-observer.js'
|
|
3
|
+
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
threshold: 0, // Float between 0 to 1.
|
|
6
|
+
tolerance: 0.1, // Float between 0 to 1. Tolerance for event firing
|
|
7
|
+
throttle: 16, // Throttle interval in ms (default: ~60fps)
|
|
8
|
+
once: false, // Only fire threshold callback once
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// TODO: Move to Utils/Dom
|
|
12
|
+
/**
|
|
13
|
+
* Scroll Observer - Optimized for performance
|
|
14
|
+
* @param {Element} node - The element to observe scroll events on
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {Number} options.threshold - Float between 0 to 1. When to trigger callback (0.75 = 75% down page)
|
|
17
|
+
* @param {Number} options.tolerance - Float between 0 to 1. Tolerance zone around threshold
|
|
18
|
+
* @param {Number} options.throttle - Throttle interval in ms (default: 16ms for ~60fps)
|
|
19
|
+
* @param {Boolean} options.once - Only fire threshold callback once (default: false)
|
|
20
|
+
* @param {Function} options.callback - Called on every scroll with scrollPercent
|
|
21
|
+
* @param {Function} options.onScrollDown - Called when scrolling down
|
|
22
|
+
* @param {Function} options.onScrollUp - Called when scrolling up
|
|
23
|
+
* @param {Function} options.onEnterThreshold - Called when entering threshold zone
|
|
24
|
+
*/
|
|
25
|
+
export function scrollObserver(node, options = {}) {
|
|
26
|
+
const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } =
|
|
27
|
+
options
|
|
28
|
+
const opts = { ...defaultOptions, ...userOpts }
|
|
29
|
+
const { threshold, tolerance, throttle, once } = opts
|
|
30
|
+
|
|
31
|
+
const prevScrollDirection = null
|
|
32
|
+
let prevScrollTop = 0
|
|
33
|
+
let prevScrollPercent = 0
|
|
34
|
+
let lastThrottleTime = 0
|
|
35
|
+
let thresholdFired = false
|
|
36
|
+
let rafId = null
|
|
37
|
+
|
|
38
|
+
// Determine scroll context once at initialization
|
|
39
|
+
const isDocumentScroll = node === document || node === window
|
|
40
|
+
const scrollElement = isDocumentScroll ? document.documentElement : node
|
|
41
|
+
|
|
42
|
+
// Cache DOM references and expensive calculations
|
|
43
|
+
let cachedScrollHeight = 0
|
|
44
|
+
let cachedClientHeight = 0
|
|
45
|
+
|
|
46
|
+
// Initialize cache and start observing for changes
|
|
47
|
+
updateCache()
|
|
48
|
+
const cacheObserver = resizeObserver(scrollElement, {
|
|
49
|
+
callback: updateCache,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
node.addEventListener('scroll', throttledObserve, { passive: true })
|
|
53
|
+
|
|
54
|
+
function updateCache() {
|
|
55
|
+
cachedScrollHeight = scrollElement.scrollHeight
|
|
56
|
+
cachedClientHeight = scrollElement.clientHeight
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function throttledObserve() {
|
|
60
|
+
const now = Date.now()
|
|
61
|
+
if (now - lastThrottleTime < throttle) return
|
|
62
|
+
|
|
63
|
+
lastThrottleTime = now
|
|
64
|
+
rafId = requestAnimationFrame(observe)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function observe() {
|
|
68
|
+
const scrollTop = scrollElement.scrollTop
|
|
69
|
+
|
|
70
|
+
// Skip if scroll position hasn't changed meaningfully
|
|
71
|
+
if (Math.abs(scrollTop - prevScrollTop) < 1) return
|
|
72
|
+
|
|
73
|
+
const scrollDirection = scrollTop > prevScrollTop ? 'down' : 'up'
|
|
74
|
+
const maxScroll = Math.max(1, cachedScrollHeight - cachedClientHeight)
|
|
75
|
+
const scrollPercent = Math.min(1, Math.max(0, scrollTop / maxScroll))
|
|
76
|
+
|
|
77
|
+
// Check threshold crossing (e.g., 75% ± tolerance)
|
|
78
|
+
const thresholdMin = threshold - tolerance / 2
|
|
79
|
+
const thresholdMax = threshold + tolerance / 2
|
|
80
|
+
const wasInThreshold =
|
|
81
|
+
prevScrollPercent >= thresholdMin && prevScrollPercent <= thresholdMax
|
|
82
|
+
const isInThreshold =
|
|
83
|
+
scrollPercent >= thresholdMin && scrollPercent <= thresholdMax
|
|
84
|
+
const hasEnteredThreshold =
|
|
85
|
+
!wasInThreshold && isInThreshold && (!once || !thresholdFired)
|
|
86
|
+
|
|
87
|
+
if (hasEnteredThreshold && once) {
|
|
88
|
+
thresholdFired = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prepare common data for all callbacks
|
|
92
|
+
const callbackData = {
|
|
93
|
+
scrollTop,
|
|
94
|
+
scrollDirection,
|
|
95
|
+
scrollPercent,
|
|
96
|
+
directionChanged: scrollDirection !== prevScrollDirection,
|
|
97
|
+
hasEnteredThreshold,
|
|
98
|
+
isInThreshold,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Call specific callbacks
|
|
102
|
+
if (typeof callback === 'function') {
|
|
103
|
+
callback(callbackData)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only fire direction callbacks when direction actually changes
|
|
107
|
+
if (scrollDirection !== prevScrollDirection) {
|
|
108
|
+
if (scrollDirection === 'down' && typeof onScrollDown === 'function') {
|
|
109
|
+
onScrollDown(callbackData)
|
|
110
|
+
}
|
|
111
|
+
if (scrollDirection === 'up' && typeof onScrollUp === 'function') {
|
|
112
|
+
onScrollUp(callbackData)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fire threshold callback when entering threshold zone
|
|
117
|
+
if (hasEnteredThreshold && typeof onEnterThreshold === 'function') {
|
|
118
|
+
onEnterThreshold(callbackData)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
prevScrollTop = scrollTop
|
|
122
|
+
prevScrollPercent = scrollPercent
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
destroy() {
|
|
127
|
+
node.removeEventListener('scroll', throttledObserve)
|
|
128
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
129
|
+
cacheObserver.destroy()
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely executes a callback function if it's a function
|
|
3
|
+
* @param {Function|*} fn - The potential callback function
|
|
4
|
+
* @param {...*} data - Data arguments to pass to the callback
|
|
5
|
+
*/
|
|
6
|
+
export function runCallback(fn, ...data) {
|
|
7
|
+
if (typeof fn === 'function') fn(data)
|
|
8
|
+
}
|