@splendidlabz/utils 1.9.1 → 1.10.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/dom/actions/index.cjs +75 -73
  3. package/dist/cjs/dom/actions/masonry.cjs +4 -2
  4. package/dist/cjs/dom/actions/prefer-horizontal-scroll.cjs +24 -24
  5. package/dist/cjs/dom/actions/sticky.cjs +4 -2
  6. package/dist/cjs/dom/font-size.cjs +9 -3
  7. package/dist/cjs/dom/index.cjs +81 -66
  8. package/dist/cjs/dom/observers/index.cjs +24 -17
  9. package/dist/cjs/dom/observers/resize-observer.cjs +4 -2
  10. package/dist/cjs/dom/observers/scroll-observer.cjs +24 -17
  11. package/dist/esm/dom/actions/index.js +75 -73
  12. package/dist/esm/dom/actions/masonry.js +4 -2
  13. package/dist/esm/dom/actions/prefer-horizontal-scroll.js +24 -24
  14. package/dist/esm/dom/actions/sticky.js +4 -2
  15. package/dist/esm/dom/font-size.js +8 -3
  16. package/dist/esm/dom/index.js +80 -66
  17. package/dist/esm/dom/observers/index.js +24 -17
  18. package/dist/esm/dom/observers/resize-observer.js +4 -2
  19. package/dist/esm/dom/observers/scroll-observer.js +24 -17
  20. package/dist/types/dom/events.d.cts +2 -2
  21. package/dist/types/dom/font-size.d.cts +3 -2
  22. package/dist/types/dom/index.d.cts +2 -2
  23. package/dist/types/dom/observers/index.d.cts +1 -1
  24. package/dist/types/dom/observers/resize-observer.d.cts +3 -3
  25. package/dist/types/dom/observers/scroll-observer.d.cts +34 -21
  26. package/dist/types/dom/spam-prevention.d.cts +7 -7
  27. package/dist/types/lib/checks.d.cts +1 -1
  28. package/package.json +1 -1
  29. package/src/dom/actions/prefer-horizontal-scroll.js +3 -8
  30. package/src/dom/events.js +1 -1
  31. package/src/dom/font-size.js +9 -4
  32. package/src/dom/observers/resize-observer.js +6 -4
  33. package/src/dom/observers/scroll-observer.js +41 -27
  34. package/src/dom/spam-prevention.js +5 -4
  35. package/src/dom/events.test.js +0 -99
@@ -102,12 +102,14 @@ function mutationObserver(target, options) {
102
102
  function resizeObserver(target, options) {
103
103
  const { callback, ...opts } = options;
104
104
  const observer = new ResizeObserver(observerFn);
105
- if (target === window) target = document.body;
105
+ if (target === window || target === document)
106
+ target = document.documentElement;
106
107
  useObserverMethodOnTarget(target, observer, "observe", opts);
107
108
  function observerFn(entries) {
108
109
  for (const entry of entries) {
109
110
  if (callback) callback({ entry, entries, observer });
110
- else dispatchEvent(target, "resize-obs", { entry, entries, observer });
111
+ else
112
+ dispatchEvent(entry.target, "resize-obs", { entry, entries, observer });
111
113
  }
112
114
  }
113
115
  return {
@@ -128,40 +130,44 @@ var defaultOptions2 = {
128
130
  // Float between 0 to 1.
129
131
  tolerance: 0.1,
130
132
  // Float between 0 to 1. Tolerance for event firing
131
- throttle: 16,
132
- // Throttle interval in ms (default: ~60fps)
133
133
  once: false
134
134
  // Only fire threshold callback once
135
135
  };
136
+ function getScrollElement(node) {
137
+ if (node === document || node === window || node === document.documentElement) {
138
+ return document.documentElement;
139
+ }
140
+ return (
141
+ /** @type {Element} */
142
+ node
143
+ );
144
+ }
136
145
  function scrollObserver(node, options = {}) {
137
146
  const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } = options;
138
147
  const opts = { ...defaultOptions2, ...userOpts };
139
- const { threshold, tolerance, throttle, once } = opts;
140
- const prevScrollDirection = null;
148
+ const { threshold, tolerance, once } = opts;
149
+ let prevScrollDirection = null;
141
150
  let prevScrollTop = 0;
142
151
  let prevScrollPercent = 0;
143
- let lastThrottleTime = 0;
144
152
  let thresholdFired = false;
145
153
  let rafId = null;
146
- const isDocumentScroll = node === document || node === window;
147
- const scrollElement = isDocumentScroll ? document.documentElement : node;
154
+ const isDocumentScroll = node === document || node === window || node === document.documentElement;
155
+ const scrollElement = getScrollElement(node);
156
+ const eventTarget = isDocumentScroll ? window : node;
148
157
  let cachedScrollHeight = 0;
149
158
  let cachedClientHeight = 0;
150
159
  updateCache();
151
160
  const cacheObserver = resizeObserver(scrollElement, {
152
161
  callback: updateCache
153
162
  });
154
- node.addEventListener("scroll", throttledObserve, { passive: true });
163
+ eventTarget.addEventListener("scroll", throttledObserve, { passive: true });
164
+ function throttledObserve() {
165
+ rafId = requestAnimationFrame(observe);
166
+ }
155
167
  function updateCache() {
156
168
  cachedScrollHeight = scrollElement.scrollHeight;
157
169
  cachedClientHeight = scrollElement.clientHeight;
158
170
  }
159
- function throttledObserve() {
160
- const now = Date.now();
161
- if (now - lastThrottleTime < throttle) return;
162
- lastThrottleTime = now;
163
- rafId = requestAnimationFrame(observe);
164
- }
165
171
  function observe() {
166
172
  const scrollTop = scrollElement.scrollTop;
167
173
  if (Math.abs(scrollTop - prevScrollTop) < 1) return;
@@ -200,10 +206,11 @@ function scrollObserver(node, options = {}) {
200
206
  }
201
207
  prevScrollTop = scrollTop;
202
208
  prevScrollPercent = scrollPercent;
209
+ prevScrollDirection = scrollDirection;
203
210
  }
204
211
  return {
205
212
  destroy() {
206
- node.removeEventListener("scroll", throttledObserve);
213
+ eventTarget.removeEventListener("scroll", throttledObserve);
207
214
  if (rafId) cancelAnimationFrame(rafId);
208
215
  cacheObserver.destroy();
209
216
  }
@@ -32,12 +32,14 @@ function useObserverMethodOnTarget(target, observer, method = "observe", options
32
32
  function resizeObserver(target, options) {
33
33
  const { callback, ...opts } = options;
34
34
  const observer = new ResizeObserver(observerFn);
35
- if (target === window) target = document.body;
35
+ if (target === window || target === document)
36
+ target = document.documentElement;
36
37
  useObserverMethodOnTarget(target, observer, "observe", opts);
37
38
  function observerFn(entries) {
38
39
  for (const entry of entries) {
39
40
  if (callback) callback({ entry, entries, observer });
40
- else dispatchEvent(target, "resize-obs", { entry, entries, observer });
41
+ else
42
+ dispatchEvent(entry.target, "resize-obs", { entry, entries, observer });
41
43
  }
42
44
  }
43
45
  return {
@@ -32,12 +32,14 @@ function useObserverMethodOnTarget(target, observer, method = "observe", options
32
32
  function resizeObserver(target, options) {
33
33
  const { callback, ...opts } = options;
34
34
  const observer = new ResizeObserver(observerFn);
35
- if (target === window) target = document.body;
35
+ if (target === window || target === document)
36
+ target = document.documentElement;
36
37
  useObserverMethodOnTarget(target, observer, "observe", opts);
37
38
  function observerFn(entries) {
38
39
  for (const entry of entries) {
39
40
  if (callback) callback({ entry, entries, observer });
40
- else dispatchEvent(target, "resize-obs", { entry, entries, observer });
41
+ else
42
+ dispatchEvent(entry.target, "resize-obs", { entry, entries, observer });
41
43
  }
42
44
  }
43
45
  return {
@@ -58,40 +60,44 @@ var defaultOptions = {
58
60
  // Float between 0 to 1.
59
61
  tolerance: 0.1,
60
62
  // Float between 0 to 1. Tolerance for event firing
61
- throttle: 16,
62
- // Throttle interval in ms (default: ~60fps)
63
63
  once: false
64
64
  // Only fire threshold callback once
65
65
  };
66
+ function getScrollElement(node) {
67
+ if (node === document || node === window || node === document.documentElement) {
68
+ return document.documentElement;
69
+ }
70
+ return (
71
+ /** @type {Element} */
72
+ node
73
+ );
74
+ }
66
75
  function scrollObserver(node, options = {}) {
67
76
  const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } = options;
68
77
  const opts = { ...defaultOptions, ...userOpts };
69
- const { threshold, tolerance, throttle, once } = opts;
70
- const prevScrollDirection = null;
78
+ const { threshold, tolerance, once } = opts;
79
+ let prevScrollDirection = null;
71
80
  let prevScrollTop = 0;
72
81
  let prevScrollPercent = 0;
73
- let lastThrottleTime = 0;
74
82
  let thresholdFired = false;
75
83
  let rafId = null;
76
- const isDocumentScroll = node === document || node === window;
77
- const scrollElement = isDocumentScroll ? document.documentElement : node;
84
+ const isDocumentScroll = node === document || node === window || node === document.documentElement;
85
+ const scrollElement = getScrollElement(node);
86
+ const eventTarget = isDocumentScroll ? window : node;
78
87
  let cachedScrollHeight = 0;
79
88
  let cachedClientHeight = 0;
80
89
  updateCache();
81
90
  const cacheObserver = resizeObserver(scrollElement, {
82
91
  callback: updateCache
83
92
  });
84
- node.addEventListener("scroll", throttledObserve, { passive: true });
93
+ eventTarget.addEventListener("scroll", throttledObserve, { passive: true });
94
+ function throttledObserve() {
95
+ rafId = requestAnimationFrame(observe);
96
+ }
85
97
  function updateCache() {
86
98
  cachedScrollHeight = scrollElement.scrollHeight;
87
99
  cachedClientHeight = scrollElement.clientHeight;
88
100
  }
89
- function throttledObserve() {
90
- const now = Date.now();
91
- if (now - lastThrottleTime < throttle) return;
92
- lastThrottleTime = now;
93
- rafId = requestAnimationFrame(observe);
94
- }
95
101
  function observe() {
96
102
  const scrollTop = scrollElement.scrollTop;
97
103
  if (Math.abs(scrollTop - prevScrollTop) < 1) return;
@@ -130,10 +136,11 @@ function scrollObserver(node, options = {}) {
130
136
  }
131
137
  prevScrollTop = scrollTop;
132
138
  prevScrollPercent = scrollPercent;
139
+ prevScrollDirection = scrollDirection;
133
140
  }
134
141
  return {
135
142
  destroy() {
136
- node.removeEventListener("scroll", throttledObserve);
143
+ eventTarget.removeEventListener("scroll", throttledObserve);
137
144
  if (rafId) cancelAnimationFrame(rafId);
138
145
  cacheObserver.destroy();
139
146
  }
@@ -2,7 +2,7 @@
2
2
  * @typedef {Object} EventListener
3
3
  * @property {Element} node - The DOM element to attach the event listener to
4
4
  * @property {string} event - The event type (e.g., 'click', 'keydown')
5
- * @property {Function} handler - The event handler function
5
+ * @property {EventListenerOrEventListenerObject} handler - The event handler function
6
6
  */
7
7
  /**
8
8
  * @typedef {Object} ListenerManager
@@ -54,7 +54,7 @@ type EventListener = {
54
54
  /**
55
55
  * - The event handler function
56
56
  */
57
- handler: Function;
57
+ handler: EventListenerOrEventListenerObject;
58
58
  };
59
59
  type ListenerManager = {
60
60
  /**
@@ -1,7 +1,8 @@
1
1
  declare function getUnit(value: any): any;
2
- declare function em(element?: HTMLElement, multiple?: number): number;
3
2
  declare function rem(multiple?: number): number;
3
+ declare function em(element?: HTMLElement, multiple?: number): number;
4
+ declare function rlh(multiple?: number): number;
4
5
  declare function lh(element?: HTMLElement, multiple?: number): number;
5
6
  declare function toPx(value: any, element?: any): number;
6
7
 
7
- export { em, getUnit, lh, rem, toPx };
8
+ export { em, getUnit, lh, rem, rlh, toPx };
@@ -8,7 +8,7 @@ export { cookies, getCookie } from './cookie.cjs';
8
8
  export { getCSSValue, getCSSVar, setCSSValue, setCSSVar } from './css-vars.cjs';
9
9
  export { CustomEventOptions, EventListener, ListenerManager, addListeners, dispatchEvent, removeListeners, updateEvent } from './events.cjs';
10
10
  export { Focusable, Focusables, getFocusableElements } from './focusable.cjs';
11
- export { em, getUnit, lh, rem, toPx } from './font-size.cjs';
11
+ export { em, getUnit, lh, rem, rlh, toPx } from './font-size.cjs';
12
12
  export { getAncestorWithSiblings, getChildrenElements, getElement, getNodeType, getParentElement, getSelfIndex, getSiblingElements, isAncestor } from './get-element.cjs';
13
13
  export { sha256Hash } from './hash.cjs';
14
14
  export { isArrowKey, isEscapeKey, isShiftTab, isTab, isTabKey } from './keyboard.cjs';
@@ -17,7 +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
+ export { ScrollObserverOptions, scrollObserver } from './observers/scroll-observer.cjs';
21
21
  export { PKCE } from './pkce.cjs';
22
22
  export { queryParams } from './query-params.cjs';
23
23
  export { randomString, uuid } from './random-string.cjs';
@@ -1,4 +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';
4
+ export { ScrollObserverOptions, scrollObserver } from './scroll-observer.cjs';
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Creates and manages a ResizeObserver instance to monitor size changes of a target element.
3
3
  *
4
- * @param {Element|Window|NodeList|Element[]} target - The element(s) to observe.
5
- * - If window is provided, document.body will be observed instead.
4
+ * @param {Element|Window|Document|NodeList|Element[]} target - The element(s) to observe.
5
+ * - If window or document is provided, document.documentElement will be observed instead.
6
6
  * - If NodeList or Array of elements is provided, all elements will be observed.
7
7
  * @param {Object} options - Configuration options for the resize observer
8
8
  * @param {boolean} [options.observe=true] - Whether to start observing immediately. If false, the observer won't be created.
@@ -31,7 +31,7 @@
31
31
  * console.log('Element resized:', detail.entry.contentRect);
32
32
  * });
33
33
  */
34
- declare function resizeObserver(target: Element | Window | NodeList | Element[], options: {
34
+ declare function resizeObserver(target: Element | Window | Document | NodeList | Element[], options: {
35
35
  observe?: boolean;
36
36
  callback?: Function;
37
37
  observerOptions?: any;
@@ -1,27 +1,40 @@
1
1
  /**
2
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
3
+ * @param {Element | Document | Window} node - The element to observe scroll events on
4
+ * @param {ScrollObserverOptions} [options] - Configuration options
13
5
  */
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
- }): {
6
+ declare function scrollObserver(node: Element | Document | Window, options?: ScrollObserverOptions): {
24
7
  destroy(): void;
25
8
  };
9
+ type ScrollObserverOptions = {
10
+ /**
11
+ * - Float between 0 to 1. When to trigger callback (0.75 = 75% down page)
12
+ */
13
+ threshold?: number;
14
+ /**
15
+ * - Float between 0 to 1. Tolerance zone around threshold
16
+ */
17
+ tolerance?: number;
18
+ /**
19
+ * - Only fire threshold callback once (default: false)
20
+ */
21
+ once?: boolean;
22
+ /**
23
+ * - Called on every scroll with scrollPercent
24
+ */
25
+ callback?: Function;
26
+ /**
27
+ * - Called when scrolling down
28
+ */
29
+ onScrollDown?: Function;
30
+ /**
31
+ * - Called when scrolling up
32
+ */
33
+ onScrollUp?: Function;
34
+ /**
35
+ * - Called when entering threshold zone
36
+ */
37
+ onEnterThreshold?: Function;
38
+ };
26
39
 
27
- export { scrollObserver };
40
+ export { type ScrollObserverOptions, scrollObserver };
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Enhances a form element with spam detection capabilities
3
3
  * @param {HTMLFormElement} form - The form element to enhance
4
- * @param {Object} options - Configuration options
5
- * @param {string} options.honeypotField - Name of the honeypot field (default: 'hp')
6
- * @param {number} options.honeypotDuration - Minimum time in ms for legitimate form fill (default: 2000)
7
- * @returns {Function} Cleanup function to remove listeners and method
4
+ * @param {Object} [options] - Configuration options
5
+ * @param {string} [options.honeypotField] - Name of the honeypot field (default: 'hp')
6
+ * @param {number} [options.honeypotDuration] - Minimum time in ms for legitimate form fill (default: 2000)
7
+ * @returns {void}
8
8
  */
9
9
  declare function preventSpam(form: HTMLFormElement, { honeypotField, honeypotDuration }?: {
10
- honeypotField: string;
11
- honeypotDuration: number;
12
- }): Function;
10
+ honeypotField?: string;
11
+ honeypotDuration?: number;
12
+ }): void;
13
13
 
14
14
  export { preventSpam };
@@ -27,6 +27,6 @@ declare function isArray(x: unknown): boolean;
27
27
  * notObject({}) // false
28
28
  */
29
29
  declare function notObject(x: unknown): boolean;
30
- declare function getType(x: any): "array" | "string" | "integer" | "float" | "boolean" | "function" | "undefined" | "symbol" | "bigint" | "object" | "unknown";
30
+ declare function getType(x: any): "string" | "integer" | "float" | "boolean" | "function" | "undefined" | "symbol" | "bigint" | "array" | "object" | "unknown";
31
31
 
32
32
  export { getType, isArray, isObject, notObject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splendidlabz/utils",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,11 +1,6 @@
1
- import {
2
- addListeners,
3
- getCSSVar,
4
- removeListeners,
5
- setCSSVar,
6
- } from '../index.js'
7
-
8
- import { omitEmpty } from '../../lib/index.js'
1
+ import { omitEmpty } from '../../lib/objects/omit-empty.js'
2
+ import { getCSSVar, setCSSVar } from '../css-vars.js'
3
+ import { addListeners, removeListeners } from '../events.js'
9
4
 
10
5
  const DEFAULT_OPTIONS = {
11
6
  scrollSnapDelay: 1000,
package/src/dom/events.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @typedef {Object} EventListener
3
3
  * @property {Element} node - The DOM element to attach the event listener to
4
4
  * @property {string} event - The event type (e.g., 'click', 'keydown')
5
- * @property {Function} handler - The event handler function
5
+ * @property {EventListenerOrEventListenerObject} handler - The event handler function
6
6
  */
7
7
 
8
8
  /**
@@ -1,18 +1,23 @@
1
- import { splitUnit } from '../lib/index.js'
1
+ import { splitUnit } from '../lib/numbers/index.js'
2
2
 
3
3
  export function getUnit(value) {
4
4
  const [, unit] = splitUnit(value)
5
5
  return unit
6
6
  }
7
7
 
8
+ export function rem(multiple = 1) {
9
+ const value = parseFloat(getComputedStyle(document.body)['font-size'])
10
+ return Math.round(value * multiple)
11
+ }
12
+
8
13
  export function em(element = document.body, multiple = 1) {
9
14
  const value = parseFloat(getComputedStyle(element)['font-size'])
10
15
  return Math.round(value * multiple)
11
16
  }
12
17
 
13
- export function rem(multiple = 1) {
14
- const value = parseFloat(getComputedStyle(document.body)['font-size'])
15
- return Math.round(value * multiple)
18
+ export function rlh(multiple = 1) {
19
+ const lineHeight = parseFloat(getComputedStyle(document.body)['line-height'])
20
+ return Math.round(lineHeight * multiple)
16
21
  }
17
22
 
18
23
  export function lh(element = document.body, multiple = 1) {
@@ -5,8 +5,8 @@ import { useObserverMethodOnTarget } from './observer.js'
5
5
  /**
6
6
  * Creates and manages a ResizeObserver instance to monitor size changes of a target element.
7
7
  *
8
- * @param {Element|Window|NodeList|Element[]} target - The element(s) to observe.
9
- * - If window is provided, document.body will be observed instead.
8
+ * @param {Element|Window|Document|NodeList|Element[]} target - The element(s) to observe.
9
+ * - If window or document is provided, document.documentElement 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
12
12
  * @param {boolean} [options.observe=true] - Whether to start observing immediately. If false, the observer won't be created.
@@ -39,13 +39,15 @@ export function resizeObserver(target, options) {
39
39
  const { callback, ...opts } = options
40
40
  const observer = new ResizeObserver(observerFn)
41
41
 
42
- if (target === window) target = document.body
42
+ if (target === window || target === document)
43
+ target = document.documentElement
43
44
  useObserverMethodOnTarget(target, observer, 'observe', opts)
44
45
 
45
46
  function observerFn(entries) {
46
47
  for (const entry of entries) {
47
48
  if (callback) callback({ entry, entries, observer })
48
- else dispatchEvent(target, 'resize-obs', { entry, entries, observer })
49
+ else
50
+ dispatchEvent(entry.target, 'resize-obs', { entry, entries, observer })
49
51
  }
50
52
  }
51
53
 
@@ -4,40 +4,57 @@ import { resizeObserver } from './resize-observer.js'
4
4
  const defaultOptions = {
5
5
  threshold: 0, // Float between 0 to 1.
6
6
  tolerance: 0.1, // Float between 0 to 1. Tolerance for event firing
7
- throttle: 16, // Throttle interval in ms (default: ~60fps)
8
7
  once: false, // Only fire threshold callback once
9
8
  }
10
9
 
11
- // TODO: Move to Utils/Dom
10
+ /**
11
+ * @typedef {Object} ScrollObserverOptions
12
+ * @property {number} [threshold] - Float between 0 to 1. When to trigger callback (0.75 = 75% down page)
13
+ * @property {number} [tolerance] - Float between 0 to 1. Tolerance zone around threshold
14
+ * @property {boolean} [once] - Only fire threshold callback once (default: false)
15
+ * @property {Function} [callback] - Called on every scroll with scrollPercent
16
+ * @property {Function} [onScrollDown] - Called when scrolling down
17
+ * @property {Function} [onScrollUp] - Called when scrolling up
18
+ * @property {Function} [onEnterThreshold] - Called when entering threshold zone
19
+ */
20
+
21
+ /**
22
+ * @param {Element | Document | Window} node
23
+ * @returns {Element}
24
+ */
25
+ function getScrollElement(node) {
26
+ if (
27
+ node === document ||
28
+ node === window ||
29
+ node === document.documentElement
30
+ ) {
31
+ return document.documentElement
32
+ }
33
+ return /** @type {Element} */ (node)
34
+ }
35
+
12
36
  /**
13
37
  * 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
38
+ * @param {Element | Document | Window} node - The element to observe scroll events on
39
+ * @param {ScrollObserverOptions} [options] - Configuration options
24
40
  */
25
41
  export function scrollObserver(node, options = {}) {
26
42
  const { callback, onScrollDown, onScrollUp, onEnterThreshold, ...userOpts } =
27
43
  options
28
44
  const opts = { ...defaultOptions, ...userOpts }
29
- const { threshold, tolerance, throttle, once } = opts
45
+ const { threshold, tolerance, once } = opts
30
46
 
31
- const prevScrollDirection = null
47
+ let prevScrollDirection = null
32
48
  let prevScrollTop = 0
33
49
  let prevScrollPercent = 0
34
- let lastThrottleTime = 0
35
50
  let thresholdFired = false
36
51
  let rafId = null
37
52
 
38
53
  // Determine scroll context once at initialization
39
- const isDocumentScroll = node === document || node === window
40
- const scrollElement = isDocumentScroll ? document.documentElement : node
54
+ const isDocumentScroll =
55
+ node === document || node === window || node === document.documentElement
56
+ const scrollElement = getScrollElement(node)
57
+ const eventTarget = isDocumentScroll ? window : node
41
58
 
42
59
  // Cache DOM references and expensive calculations
43
60
  let cachedScrollHeight = 0
@@ -49,21 +66,17 @@ export function scrollObserver(node, options = {}) {
49
66
  callback: updateCache,
50
67
  })
51
68
 
52
- node.addEventListener('scroll', throttledObserve, { passive: true })
69
+ eventTarget.addEventListener('scroll', throttledObserve, { passive: true })
70
+
71
+ function throttledObserve() {
72
+ rafId = requestAnimationFrame(observe)
73
+ }
53
74
 
54
75
  function updateCache() {
55
76
  cachedScrollHeight = scrollElement.scrollHeight
56
77
  cachedClientHeight = scrollElement.clientHeight
57
78
  }
58
79
 
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
80
  function observe() {
68
81
  const scrollTop = scrollElement.scrollTop
69
82
 
@@ -120,11 +133,12 @@ export function scrollObserver(node, options = {}) {
120
133
 
121
134
  prevScrollTop = scrollTop
122
135
  prevScrollPercent = scrollPercent
136
+ prevScrollDirection = scrollDirection
123
137
  }
124
138
 
125
139
  return {
126
140
  destroy() {
127
- node.removeEventListener('scroll', throttledObserve)
141
+ eventTarget.removeEventListener('scroll', throttledObserve)
128
142
  if (rafId) cancelAnimationFrame(rafId)
129
143
  cacheObserver.destroy()
130
144
  },
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Enhances a form element with spam detection capabilities
3
3
  * @param {HTMLFormElement} form - The form element to enhance
4
- * @param {Object} options - Configuration options
5
- * @param {string} options.honeypotField - Name of the honeypot field (default: 'hp')
6
- * @param {number} options.honeypotDuration - Minimum time in ms for legitimate form fill (default: 2000)
7
- * @returns {Function} Cleanup function to remove listeners and method
4
+ * @param {Object} [options] - Configuration options
5
+ * @param {string} [options.honeypotField] - Name of the honeypot field (default: 'hp')
6
+ * @param {number} [options.honeypotDuration] - Minimum time in ms for legitimate form fill (default: 2000)
7
+ * @returns {void}
8
8
  */
9
9
  export function preventSpam(
10
10
  form,
@@ -25,6 +25,7 @@ export function preventSpam(
25
25
  form.containsSpam = function () {
26
26
  const fillTime = Date.now() - startTime
27
27
  const isTooFast = fillTime < honeypotDuration
28
+ /** @type {HTMLInputElement | null} */
28
29
  const honeypotInput = form.querySelector(`[name="${honeypotField}"]`)
29
30
  const hasHoneypotValue = honeypotInput?.value?.trim()
30
31
  const noInteraction = !hasInteraction