tiny-essentials 1.16.1 → 1.16.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.
@@ -19,8 +19,8 @@ class TinyAfterScrollWatcher {
19
19
  /** @type {Element|Window} */
20
20
  #scrollTarget;
21
21
 
22
- /** @type {number} */
23
- lastScrollTime = 0;
22
+ /** @type {null|NodeJS.Timeout} */
23
+ #lastScrollTime = null;
24
24
 
25
25
  /** @type {FnData[]} */
26
26
  #afterScrollQueue = [];
@@ -31,6 +31,9 @@ class TinyAfterScrollWatcher {
31
31
  /** @type {Set<OnScrollFunc>} */
32
32
  #externalScrollListeners = new Set();
33
33
 
34
+ /** @type {Set<FnData>} */
35
+ #onStopListeners = new Set();
36
+
34
37
  /** @type {boolean} */
35
38
  #destroyed = false;
36
39
 
@@ -44,16 +47,20 @@ class TinyAfterScrollWatcher {
44
47
  if (!(scrollTarget instanceof Element) && !(scrollTarget instanceof Window))
45
48
  throw new TypeError('scrollTarget must be an Element or the Window object.');
46
49
  this.#scrollTarget = scrollTarget;
50
+ this._checkTimer = this._checkTimer.bind(this);
47
51
 
48
- this._onScroll = this._onScroll.bind(this);
49
- this._checkQueue = this._checkQueue.bind(this);
50
-
51
- this.#scrollTarget.addEventListener('scroll', this._onScroll);
52
+ this.#scrollTarget.addEventListener('scroll', this._checkTimer);
52
53
  this.#inactivityTime = inactivityTime;
53
-
54
- requestAnimationFrame(this._checkQueue);
55
54
  }
56
55
 
56
+ _checkTimer = () => {
57
+ if (this.#lastScrollTime) clearTimeout(this.#lastScrollTime);
58
+ this.#lastScrollTime = setTimeout(() => {
59
+ this.#lastScrollTime = null;
60
+ this.#checkQueue();
61
+ }, this.#inactivityTime);
62
+ };
63
+
57
64
  /**
58
65
  * Gets the current inactivity time in milliseconds.
59
66
  * @returns {number}
@@ -74,29 +81,21 @@ class TinyAfterScrollWatcher {
74
81
  this.#inactivityTime = value;
75
82
  }
76
83
 
77
- /**
78
- * Internal handler for scroll events.
79
- * Updates the last scroll timestamp.
80
- * @private
81
- */
82
- _onScroll() {
83
- this.lastScrollTime = Date.now();
84
- }
85
-
86
84
  /**
87
85
  * Continuously checks whether the user has stopped scrolling,
88
86
  * and if so, runs all queued functions.
89
- * @private
90
87
  */
91
- _checkQueue() {
88
+ #checkQueue() {
92
89
  if (this.#destroyed) return;
93
- requestAnimationFrame(this._checkQueue);
90
+ // Runs all onStop first listeners
91
+ for (const fn of this.#onStopListeners) {
92
+ if (typeof fn === 'function') fn();
93
+ }
94
94
 
95
- if (Date.now() - this.lastScrollTime > this.#inactivityTime) {
96
- while (this.#afterScrollQueue.length) {
97
- const fn = this.#afterScrollQueue.pop();
98
- if (typeof fn === 'function') fn();
99
- }
95
+ // Then execute the queue afterScrollQueue
96
+ while (this.#afterScrollQueue.length) {
97
+ const fn = this.#afterScrollQueue.pop();
98
+ if (typeof fn === 'function') fn();
100
99
  }
101
100
  }
102
101
 
@@ -113,6 +112,29 @@ class TinyAfterScrollWatcher {
113
112
  this.#afterScrollQueue.push(fn);
114
113
  }
115
114
 
115
+ /**
116
+ * Registers a function to run once after scrolling has stopped,
117
+ * before any afterScrollQueue functions.
118
+ *
119
+ * @param {FnData} fn - A function to execute after scroll stop.
120
+ * @throws {TypeError} If the argument is not a function.
121
+ */
122
+ onStop(fn) {
123
+ if (typeof fn !== 'function') throw new TypeError('Argument must be a function.');
124
+ this.#onStopListeners.add(fn);
125
+ }
126
+
127
+ /**
128
+ * Removes a previously registered onStop function.
129
+ *
130
+ * @param {FnData} fn - The function to remove.
131
+ * @throws {TypeError} If the argument is not a function.
132
+ */
133
+ offStop(fn) {
134
+ if (typeof fn !== 'function') throw new TypeError('Argument must be a function.');
135
+ this.#onStopListeners.delete(fn);
136
+ }
137
+
116
138
  /**
117
139
  * Registers an external scroll listener on the tracked element.
118
140
  *
@@ -146,11 +168,12 @@ class TinyAfterScrollWatcher {
146
168
  if (this.#destroyed) return;
147
169
  this.#destroyed = true;
148
170
 
149
- this.#scrollTarget.removeEventListener('scroll', this._onScroll);
171
+ this.#scrollTarget.removeEventListener('scroll', this._checkTimer);
150
172
  for (const fn of this.#externalScrollListeners)
151
173
  this.#scrollTarget.removeEventListener('scroll', fn);
152
174
 
153
175
  this.#externalScrollListeners.clear();
176
+ this.#onStopListeners.clear();
154
177
  }
155
178
  }
156
179
 
@@ -29,20 +29,7 @@ declare class TinyAfterScrollWatcher {
29
29
  * @throws {TypeError} If inactivityTime is not a positive number
30
30
  */
31
31
  constructor(scrollTarget?: Element | Window, inactivityTime?: number);
32
- /** @type {number} */
33
- lastScrollTime: number;
34
- /**
35
- * Internal handler for scroll events.
36
- * Updates the last scroll timestamp.
37
- * @private
38
- */
39
- private _onScroll;
40
- /**
41
- * Continuously checks whether the user has stopped scrolling,
42
- * and if so, runs all queued functions.
43
- * @private
44
- */
45
- private _checkQueue;
32
+ _checkTimer: () => void;
46
33
  /**
47
34
  * Sets a new inactivity time.
48
35
  * Must be a positive number (in milliseconds).
@@ -63,6 +50,22 @@ declare class TinyAfterScrollWatcher {
63
50
  * @throws {TypeError} If the argument is not a function.
64
51
  */
65
52
  doAfterScroll(fn: () => void): void;
53
+ lastScrollTime: number | undefined;
54
+ /**
55
+ * Registers a function to run once after scrolling has stopped,
56
+ * before any afterScrollQueue functions.
57
+ *
58
+ * @param {FnData} fn - A function to execute after scroll stop.
59
+ * @throws {TypeError} If the argument is not a function.
60
+ */
61
+ onStop(fn: FnData): void;
62
+ /**
63
+ * Removes a previously registered onStop function.
64
+ *
65
+ * @param {FnData} fn - The function to remove.
66
+ * @throws {TypeError} If the argument is not a function.
67
+ */
68
+ offStop(fn: FnData): void;
66
69
  /**
67
70
  * Registers an external scroll listener on the tracked element.
68
71
  *
@@ -14,14 +14,16 @@
14
14
  class TinyAfterScrollWatcher {
15
15
  /** @type {Element|Window} */
16
16
  #scrollTarget;
17
- /** @type {number} */
18
- lastScrollTime = 0;
17
+ /** @type {null|NodeJS.Timeout} */
18
+ #lastScrollTime = null;
19
19
  /** @type {FnData[]} */
20
20
  #afterScrollQueue = [];
21
21
  /** @type {number} */
22
22
  #inactivityTime = 100;
23
23
  /** @type {Set<OnScrollFunc>} */
24
24
  #externalScrollListeners = new Set();
25
+ /** @type {Set<FnData>} */
26
+ #onStopListeners = new Set();
25
27
  /** @type {boolean} */
26
28
  #destroyed = false;
27
29
  /**
@@ -34,12 +36,18 @@ class TinyAfterScrollWatcher {
34
36
  if (!(scrollTarget instanceof Element) && !(scrollTarget instanceof Window))
35
37
  throw new TypeError('scrollTarget must be an Element or the Window object.');
36
38
  this.#scrollTarget = scrollTarget;
37
- this._onScroll = this._onScroll.bind(this);
38
- this._checkQueue = this._checkQueue.bind(this);
39
- this.#scrollTarget.addEventListener('scroll', this._onScroll);
39
+ this._checkTimer = this._checkTimer.bind(this);
40
+ this.#scrollTarget.addEventListener('scroll', this._checkTimer);
40
41
  this.#inactivityTime = inactivityTime;
41
- requestAnimationFrame(this._checkQueue);
42
42
  }
43
+ _checkTimer = () => {
44
+ if (this.#lastScrollTime)
45
+ clearTimeout(this.#lastScrollTime);
46
+ this.#lastScrollTime = setTimeout(() => {
47
+ this.#lastScrollTime = null;
48
+ this.#checkQueue();
49
+ }, this.#inactivityTime);
50
+ };
43
51
  /**
44
52
  * Gets the current inactivity time in milliseconds.
45
53
  * @returns {number}
@@ -58,29 +66,23 @@ class TinyAfterScrollWatcher {
58
66
  throw new Error('inactivityTime must be a positive number in milliseconds.');
59
67
  this.#inactivityTime = value;
60
68
  }
61
- /**
62
- * Internal handler for scroll events.
63
- * Updates the last scroll timestamp.
64
- * @private
65
- */
66
- _onScroll() {
67
- this.lastScrollTime = Date.now();
68
- }
69
69
  /**
70
70
  * Continuously checks whether the user has stopped scrolling,
71
71
  * and if so, runs all queued functions.
72
- * @private
73
72
  */
74
- _checkQueue() {
73
+ #checkQueue() {
75
74
  if (this.#destroyed)
76
75
  return;
77
- requestAnimationFrame(this._checkQueue);
78
- if (Date.now() - this.lastScrollTime > this.#inactivityTime) {
79
- while (this.#afterScrollQueue.length) {
80
- const fn = this.#afterScrollQueue.pop();
81
- if (typeof fn === 'function')
82
- fn();
83
- }
76
+ // Runs all onStop first listeners
77
+ for (const fn of this.#onStopListeners) {
78
+ if (typeof fn === 'function')
79
+ fn();
80
+ }
81
+ // Then execute the queue afterScrollQueue
82
+ while (this.#afterScrollQueue.length) {
83
+ const fn = this.#afterScrollQueue.pop();
84
+ if (typeof fn === 'function')
85
+ fn();
84
86
  }
85
87
  }
86
88
  /**
@@ -96,6 +98,29 @@ class TinyAfterScrollWatcher {
96
98
  this.lastScrollTime = Date.now();
97
99
  this.#afterScrollQueue.push(fn);
98
100
  }
101
+ /**
102
+ * Registers a function to run once after scrolling has stopped,
103
+ * before any afterScrollQueue functions.
104
+ *
105
+ * @param {FnData} fn - A function to execute after scroll stop.
106
+ * @throws {TypeError} If the argument is not a function.
107
+ */
108
+ onStop(fn) {
109
+ if (typeof fn !== 'function')
110
+ throw new TypeError('Argument must be a function.');
111
+ this.#onStopListeners.add(fn);
112
+ }
113
+ /**
114
+ * Removes a previously registered onStop function.
115
+ *
116
+ * @param {FnData} fn - The function to remove.
117
+ * @throws {TypeError} If the argument is not a function.
118
+ */
119
+ offStop(fn) {
120
+ if (typeof fn !== 'function')
121
+ throw new TypeError('Argument must be a function.');
122
+ this.#onStopListeners.delete(fn);
123
+ }
99
124
  /**
100
125
  * Registers an external scroll listener on the tracked element.
101
126
  *
@@ -129,10 +154,11 @@ class TinyAfterScrollWatcher {
129
154
  if (this.#destroyed)
130
155
  return;
131
156
  this.#destroyed = true;
132
- this.#scrollTarget.removeEventListener('scroll', this._onScroll);
157
+ this.#scrollTarget.removeEventListener('scroll', this._checkTimer);
133
158
  for (const fn of this.#externalScrollListeners)
134
159
  this.#scrollTarget.removeEventListener('scroll', fn);
135
160
  this.#externalScrollListeners.clear();
161
+ this.#onStopListeners.clear();
136
162
  }
137
163
  }
138
164
  export default TinyAfterScrollWatcher;
@@ -10,7 +10,8 @@ Perfect for optimizing logic that should only run **after** the user has finishe
10
10
  * ✅ Works on any scrollable element or the window
11
11
  * ⏱️ Customizable "inactivity timeout" (default: `100ms`)
12
12
  * 🧠 Executes queued callbacks after scroll ends
13
- * 🧩 Add/remove your own scroll listeners easily (`onScroll` / `offScroll`)
13
+ * 🧷 Add/remove scroll listeners easily (`onScroll` / `offScroll`)
14
+ * 🛑 Execute logic right before after-scroll queue (`onStop`)
14
15
  * 🧹 Cleanly destroy the instance when no longer needed
15
16
 
16
17
  ---
@@ -36,6 +37,11 @@ watcher.doAfterScroll(() => {
36
37
  console.log('Scroll has stopped for 300ms!');
37
38
  });
38
39
 
40
+ // Add a function to run immediately when scroll stops
41
+ watcher.onStop(() => {
42
+ console.log('User just stopped scrolling!');
43
+ });
44
+
39
45
  // Add a custom scroll listener
40
46
  const myScrollHandler = () => console.log('Scrolling...');
41
47
  watcher.onScroll(myScrollHandler);
@@ -43,6 +49,9 @@ watcher.onScroll(myScrollHandler);
43
49
  // Remove it later
44
50
  watcher.offScroll(myScrollHandler);
45
51
 
52
+ // Also remove onStop listener
53
+ watcher.offStop(myStopFunction);
54
+
46
55
  // Fully destroy the watcher
47
56
  watcher.destroy();
48
57
  ```
@@ -106,6 +115,46 @@ doAfterScroll(fn: FnData): void
106
115
 
107
116
  ---
108
117
 
118
+ ### 🛑 Method: `onStop(fn)`
119
+
120
+ Registers a function to be called **immediately after scrolling stops**, but **before** the `doAfterScroll` queue is executed.
121
+
122
+ ```ts
123
+ onStop(fn: FnData): void
124
+ ```
125
+
126
+ #### Parameters:
127
+
128
+ | Name | Type | Description |
129
+ | ---- | -------- | ----------------------------------------------- |
130
+ | `fn` | `FnData` | Function to call as soon as scroll has stopped. |
131
+
132
+ #### Throws:
133
+
134
+ * `TypeError` if the argument is not a function.
135
+
136
+ ---
137
+
138
+ ### 🔁 Method: `offStop(fn)`
139
+
140
+ Removes a function previously registered with `onStop`.
141
+
142
+ ```ts
143
+ offStop(fn: FnData): void
144
+ ```
145
+
146
+ #### Parameters:
147
+
148
+ | Name | Type | Description |
149
+ | ---- | -------- | ---------------------------------- |
150
+ | `fn` | `FnData` | Function to remove from onStop set |
151
+
152
+ #### Throws:
153
+
154
+ * `TypeError` if the argument is not a function.
155
+
156
+ ---
157
+
109
158
  ### 🧷 Method: `onScroll(fn)`
110
159
 
111
160
  Registers a custom scroll listener on the tracked element.
@@ -176,6 +225,7 @@ type OnScrollFunc = (ev: Event) => void;
176
225
 
177
226
  ## 📝 Notes
178
227
 
179
- * The scroll queue is **LIFO** (last-in, first-out)
180
- * The internal check loop uses `requestAnimationFrame`, so it's efficient
181
- * Setting `inactivityTime` to a lower value makes it more sensitive
228
+ * `onStop` functions are called **before** the `doAfterScroll` queue.
229
+ * The `doAfterScroll` queue runs in **LIFO** order (last-in, first-out).
230
+ * `requestAnimationFrame` is used for efficient polling.
231
+ * Lower `inactivityTime` makes scroll-stop detection more responsive.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
5
  "scripts": {
6
6
  "test": "npm run test:mjs && npm run test:cjs && npm run test:js",