@splendidlabz/utils 1.8.1 → 1.8.2

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.
@@ -121,8 +121,97 @@ function resizeObserver(target, options) {
121
121
  destroy: (_) => observer.disconnect()
122
122
  };
123
123
  }
124
+
125
+ // src/dom/observers/scroll-observer.js
126
+ var defaultOptions2 = {
127
+ threshold: 0,
128
+ // Float between 0 to 1.
129
+ tolerance: 0.1,
130
+ // Float between 0 to 1. Tolerance for event firing
131
+ throttle: 16,
132
+ // Throttle interval in ms (default: ~60fps)
133
+ once: false
134
+ // Only fire threshold callback once
135
+ };
136
+ function scrollObserver(node, options = {}) {
137
+ const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } = options;
138
+ const opts = { ...defaultOptions2, ...userOpts };
139
+ const { threshold, tolerance, throttle, once } = opts;
140
+ const prevScrollDirection = null;
141
+ let prevScrollTop = 0;
142
+ let prevScrollPercent = 0;
143
+ let lastThrottleTime = 0;
144
+ let thresholdFired = false;
145
+ let rafId = null;
146
+ const isDocumentScroll = node === document || node === window;
147
+ const scrollElement = isDocumentScroll ? document.documentElement : node;
148
+ let cachedScrollHeight = 0;
149
+ let cachedClientHeight = 0;
150
+ updateCache();
151
+ const cacheObserver = resizeObserver(scrollElement, {
152
+ callback: updateCache
153
+ });
154
+ node.addEventListener("scroll", throttledObserve, { passive: true });
155
+ function updateCache() {
156
+ cachedScrollHeight = scrollElement.scrollHeight;
157
+ cachedClientHeight = scrollElement.clientHeight;
158
+ }
159
+ function throttledObserve() {
160
+ const now = Date.now();
161
+ if (now - lastThrottleTime < throttle) return;
162
+ lastThrottleTime = now;
163
+ rafId = requestAnimationFrame(observe);
164
+ }
165
+ function observe() {
166
+ const scrollTop = scrollElement.scrollTop;
167
+ if (Math.abs(scrollTop - prevScrollTop) < 1) return;
168
+ const scrollDirection = scrollTop > prevScrollTop ? "down" : "up";
169
+ const maxScroll = Math.max(1, cachedScrollHeight - cachedClientHeight);
170
+ const scrollPercent = Math.min(1, Math.max(0, scrollTop / maxScroll));
171
+ const thresholdMin = threshold - tolerance / 2;
172
+ const thresholdMax = threshold + tolerance / 2;
173
+ const wasInThreshold = prevScrollPercent >= thresholdMin && prevScrollPercent <= thresholdMax;
174
+ const isInThreshold = scrollPercent >= thresholdMin && scrollPercent <= thresholdMax;
175
+ const hasEnteredThreshold = !wasInThreshold && isInThreshold && (!once || !thresholdFired);
176
+ if (hasEnteredThreshold && once) {
177
+ thresholdFired = true;
178
+ }
179
+ const callbackData = {
180
+ scrollTop,
181
+ scrollDirection,
182
+ scrollPercent,
183
+ directionChanged: scrollDirection !== prevScrollDirection,
184
+ hasEnteredThreshold,
185
+ isInThreshold
186
+ };
187
+ if (typeof callback === "function") {
188
+ callback(callbackData);
189
+ }
190
+ if (scrollDirection !== prevScrollDirection) {
191
+ if (scrollDirection === "down" && typeof onScrollDown === "function") {
192
+ onScrollDown(callbackData);
193
+ }
194
+ if (scrollDirection === "up" && typeof onScrollUp === "function") {
195
+ onScrollUp(callbackData);
196
+ }
197
+ }
198
+ if (hasEnteredThreshold && typeof onEnterThreshold === "function") {
199
+ onEnterThreshold(callbackData);
200
+ }
201
+ prevScrollTop = scrollTop;
202
+ prevScrollPercent = scrollPercent;
203
+ }
204
+ return {
205
+ destroy() {
206
+ node.removeEventListener("scroll", throttledObserve);
207
+ if (rafId) cancelAnimationFrame(rafId);
208
+ cacheObserver.destroy();
209
+ }
210
+ };
211
+ }
124
212
  export {
125
213
  intersectionObserver,
126
214
  mutationObserver,
127
- resizeObserver
215
+ resizeObserver,
216
+ scrollObserver
128
217
  };
@@ -0,0 +1,144 @@
1
+ // src/dom/events.js
2
+ function dispatchEvent(node, eventName, detail, options = {}) {
3
+ node.dispatchEvent(
4
+ new CustomEvent(eventName, {
5
+ ...options,
6
+ detail
7
+ })
8
+ );
9
+ }
10
+
11
+ // src/dom/get-element.js
12
+ function getNodeType(node) {
13
+ if (node instanceof Element) return "element";
14
+ if (node instanceof NodeList) return "nodelist";
15
+ if (Array.isArray(node)) return "array";
16
+ }
17
+
18
+ // src/dom/observers/observer.js
19
+ function useObserverMethodOnTarget(target, observer, method = "observe", options = void 0) {
20
+ const targetType = getNodeType(target);
21
+ if (targetType === "element") observer[method](target, options);
22
+ if (targetType === "nodelist") {
23
+ const elements = Array.from(target);
24
+ elements.forEach((element) => observer[method](element, options));
25
+ }
26
+ if (targetType === "array") {
27
+ target.forEach((element) => observer[method](element, options));
28
+ }
29
+ }
30
+
31
+ // src/dom/observers/resize-observer.js
32
+ function resizeObserver(target, options) {
33
+ const { callback, ...opts } = options;
34
+ const observer = new ResizeObserver(observerFn);
35
+ if (target === window) target = document.body;
36
+ useObserverMethodOnTarget(target, observer, "observe", opts);
37
+ function observerFn(entries) {
38
+ for (const entry of entries) {
39
+ if (callback) callback({ entry, entries, observer });
40
+ else dispatchEvent(target, "resize-obs", { entry, entries, observer });
41
+ }
42
+ }
43
+ return {
44
+ observe(target2) {
45
+ useObserverMethodOnTarget(target2, observer, "observe", options);
46
+ },
47
+ unobserve(target2) {
48
+ useObserverMethodOnTarget(target2, observer, "unobserve");
49
+ },
50
+ disconnect: (_) => observer.disconnect(),
51
+ destroy: (_) => observer.disconnect()
52
+ };
53
+ }
54
+
55
+ // src/dom/observers/scroll-observer.js
56
+ var defaultOptions = {
57
+ threshold: 0,
58
+ // Float between 0 to 1.
59
+ tolerance: 0.1,
60
+ // Float between 0 to 1. Tolerance for event firing
61
+ throttle: 16,
62
+ // Throttle interval in ms (default: ~60fps)
63
+ once: false
64
+ // Only fire threshold callback once
65
+ };
66
+ function scrollObserver(node, options = {}) {
67
+ const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } = options;
68
+ const opts = { ...defaultOptions, ...userOpts };
69
+ const { threshold, tolerance, throttle, once } = opts;
70
+ const prevScrollDirection = null;
71
+ let prevScrollTop = 0;
72
+ let prevScrollPercent = 0;
73
+ let lastThrottleTime = 0;
74
+ let thresholdFired = false;
75
+ let rafId = null;
76
+ const isDocumentScroll = node === document || node === window;
77
+ const scrollElement = isDocumentScroll ? document.documentElement : node;
78
+ let cachedScrollHeight = 0;
79
+ let cachedClientHeight = 0;
80
+ updateCache();
81
+ const cacheObserver = resizeObserver(scrollElement, {
82
+ callback: updateCache
83
+ });
84
+ node.addEventListener("scroll", throttledObserve, { passive: true });
85
+ function updateCache() {
86
+ cachedScrollHeight = scrollElement.scrollHeight;
87
+ cachedClientHeight = scrollElement.clientHeight;
88
+ }
89
+ function throttledObserve() {
90
+ const now = Date.now();
91
+ if (now - lastThrottleTime < throttle) return;
92
+ lastThrottleTime = now;
93
+ rafId = requestAnimationFrame(observe);
94
+ }
95
+ function observe() {
96
+ const scrollTop = scrollElement.scrollTop;
97
+ if (Math.abs(scrollTop - prevScrollTop) < 1) return;
98
+ const scrollDirection = scrollTop > prevScrollTop ? "down" : "up";
99
+ const maxScroll = Math.max(1, cachedScrollHeight - cachedClientHeight);
100
+ const scrollPercent = Math.min(1, Math.max(0, scrollTop / maxScroll));
101
+ const thresholdMin = threshold - tolerance / 2;
102
+ const thresholdMax = threshold + tolerance / 2;
103
+ const wasInThreshold = prevScrollPercent >= thresholdMin && prevScrollPercent <= thresholdMax;
104
+ const isInThreshold = scrollPercent >= thresholdMin && scrollPercent <= thresholdMax;
105
+ const hasEnteredThreshold = !wasInThreshold && isInThreshold && (!once || !thresholdFired);
106
+ if (hasEnteredThreshold && once) {
107
+ thresholdFired = true;
108
+ }
109
+ const callbackData = {
110
+ scrollTop,
111
+ scrollDirection,
112
+ scrollPercent,
113
+ directionChanged: scrollDirection !== prevScrollDirection,
114
+ hasEnteredThreshold,
115
+ isInThreshold
116
+ };
117
+ if (typeof callback === "function") {
118
+ callback(callbackData);
119
+ }
120
+ if (scrollDirection !== prevScrollDirection) {
121
+ if (scrollDirection === "down" && typeof onScrollDown === "function") {
122
+ onScrollDown(callbackData);
123
+ }
124
+ if (scrollDirection === "up" && typeof onScrollUp === "function") {
125
+ onScrollUp(callbackData);
126
+ }
127
+ }
128
+ if (hasEnteredThreshold && typeof onEnterThreshold === "function") {
129
+ onEnterThreshold(callbackData);
130
+ }
131
+ prevScrollTop = scrollTop;
132
+ prevScrollPercent = scrollPercent;
133
+ }
134
+ return {
135
+ destroy() {
136
+ node.removeEventListener("scroll", throttledObserve);
137
+ if (rafId) cancelAnimationFrame(rafId);
138
+ cacheObserver.destroy();
139
+ }
140
+ };
141
+ }
142
+ export {
143
+ scrollObserver
144
+ };
@@ -15,11 +15,27 @@ function compose(...fns) {
15
15
  return fns.reduceRight((acc, fn) => fn(acc), value);
16
16
  };
17
17
  }
18
+ function composeAsync(...fns) {
19
+ return async function(value) {
20
+ return fns.reduceRight(async (acc, fn) => {
21
+ const result = await acc;
22
+ return await fn(result);
23
+ }, value);
24
+ };
25
+ }
18
26
  function pipe(...fns) {
19
27
  return function(value) {
20
28
  return fns.reduce((acc, fn) => fn(acc), value);
21
29
  };
22
30
  }
31
+ function pipeAsync(...fns) {
32
+ return async function(value) {
33
+ return fns.reduce(async (acc, fn) => {
34
+ const result = await acc;
35
+ return await fn(result);
36
+ }, value);
37
+ };
38
+ }
23
39
  function times(fn, n) {
24
40
  const result = [];
25
41
  for (let i = 0; i < n; i++) {
@@ -29,7 +45,9 @@ function times(fn, n) {
29
45
  }
30
46
  export {
31
47
  compose,
48
+ composeAsync,
32
49
  curry,
33
50
  pipe,
51
+ pipeAsync,
34
52
  times
35
53
  };
@@ -38,11 +38,27 @@ function compose(...fns) {
38
38
  return fns.reduceRight((acc, fn) => fn(acc), value);
39
39
  };
40
40
  }
41
+ function composeAsync(...fns) {
42
+ return async function(value) {
43
+ return fns.reduceRight(async (acc, fn) => {
44
+ const result = await acc;
45
+ return await fn(result);
46
+ }, value);
47
+ };
48
+ }
41
49
  function pipe(...fns) {
42
50
  return function(value) {
43
51
  return fns.reduce((acc, fn) => fn(acc), value);
44
52
  };
45
53
  }
54
+ function pipeAsync(...fns) {
55
+ return async function(value) {
56
+ return fns.reduce(async (acc, fn) => {
57
+ const result = await acc;
58
+ return await fn(result);
59
+ }, value);
60
+ };
61
+ }
46
62
  function times(fn, n) {
47
63
  const result = [];
48
64
  for (let i = 0; i < n; i++) {
@@ -75,11 +91,13 @@ function wait(ms) {
75
91
  }
76
92
  export {
77
93
  compose,
94
+ composeAsync,
78
95
  curry,
79
96
  debounce,
80
97
  delay,
81
98
  getEnv,
82
99
  pipe,
100
+ pipeAsync,
83
101
  throttle,
84
102
  timeout,
85
103
  times,
@@ -450,11 +450,27 @@ function compose(...fns) {
450
450
  return fns.reduceRight((acc, fn) => fn(acc), value);
451
451
  };
452
452
  }
453
+ function composeAsync(...fns) {
454
+ return async function(value) {
455
+ return fns.reduceRight(async (acc, fn) => {
456
+ const result = await acc;
457
+ return await fn(result);
458
+ }, value);
459
+ };
460
+ }
453
461
  function pipe(...fns) {
454
462
  return function(value) {
455
463
  return fns.reduce((acc, fn) => fn(acc), value);
456
464
  };
457
465
  }
466
+ function pipeAsync(...fns) {
467
+ return async function(value) {
468
+ return fns.reduce(async (acc, fn) => {
469
+ const result = await acc;
470
+ return await fn(result);
471
+ }, value);
472
+ };
473
+ }
458
474
  function times(fn, n) {
459
475
  const result = [];
460
476
  for (let i = 0; i < n; i++) {
@@ -946,6 +962,7 @@ export {
946
962
  RouteManager,
947
963
  camelCaseKeys,
948
964
  compose,
965
+ composeAsync,
949
966
  concatMix,
950
967
  createMix,
951
968
  createSSE,
@@ -987,6 +1004,7 @@ export {
987
1004
  parseJSON,
988
1005
  parseSSE,
989
1006
  pipe,
1007
+ pipeAsync,
990
1008
  plural,
991
1009
  pluralize,
992
1010
  reject,
@@ -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';
@@ -1,3 +1,4 @@
1
1
  export { intersectionObserver } from './intersection-observer.cjs';
2
2
  export { mutationObserver } from './mutation-observer.cjs';
3
3
  export { resizeObserver } from './resize-observer.cjs';
4
+ export { scrollObserver } from './scroll-observer.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 };
@@ -1,6 +1,72 @@
1
- declare function curry(fn: any): (...args: any[]) => any;
2
- declare function compose(...fns: any[]): (value: any) => any;
3
- declare function pipe(...fns: any[]): (value: any) => any;
4
- declare function times(fn: any, n: any): any[];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splendidlabz/utils",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,3 +1,4 @@
1
1
  export * from './intersection-observer.js'
2
2
  export * from './mutation-observer.js'
3
3
  export * from './resize-observer.js'
4
+ export * from './scroll-observer.js'
@@ -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);