js.foresight 3.1.1 → 3.1.3

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Bart Spaans
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bart Spaans
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.md CHANGED
@@ -1,132 +1,122 @@
1
- # [ForesightJS](https://foresightjs.com/)
2
-
3
- [![npm version](https://img.shields.io/npm/v/js.foresight.svg)](https://www.npmjs.com/package/js.foresight)
4
- [![npm downloads](https://img.shields.io/npm/dt/js.foresight.svg)](https://www.npmjs.com/package/js.foresight)
5
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/js.foresight)](https://bundlephobia.com/package/js.foresight)
6
- [![GitHub stars](https://img.shields.io/github/stars/spaansba/ForesightJS.svg?style=social&label=Star)](https://github.com/spaansba/ForesightJS)
7
- [![GitHub last commit](https://img.shields.io/github/last-commit/spaansba/ForesightJS)](https://github.com/spaansba/ForesightJS/commits)
8
-
9
- [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
- [![Demo](https://img.shields.io/badge/demo-live-blue)](https://foresightjs.com#playground)
12
-
13
- ForesightJS is a lightweight JavaScript library with full TypeScript support that predicts user intent based on mouse movements, scroll and keyboard navigation. By analyzing cursor/scroll trajectory and tab sequences, it anticipates which elements a user is likely to interact with, allowing developers to trigger actions before the actual hover or click occurs (for example prefetching).
14
-
15
- ### Understanding ForesightJS's Role:
16
-
17
- When you over simplify prefetching it exists of three parts.
18
-
19
- - **What** resource or data to load
20
- - **How** the loading method and caching strategy is
21
- - **When** the optimal moment to start fetching is
22
-
23
- ForesightJS takes care of the **When** by predicting user intent with mouse trajectory and tab navigation.
24
- You supply the **What** and **How** inside your `callback` when you register an element.
25
-
26
- ### [Playground](https://foresightjs.com/)
27
-
28
- ![](https://github.com/user-attachments/assets/36c81a82-fee7-43d6-ba1e-c48214136f90)
29
- _In the GIF above, the [ForesightJS DevTools](https://foresightjs.com/docs/getting_started/development_tools) are enabled. Normally, users won't see anything that ForesightJS does except the increased perceived speed from early prefetching._
30
-
31
- ## Download
32
-
33
- ```bash
34
- pnpm add js.foresight
35
- # or
36
- npm install js.foresight
37
- # or
38
- yarn add js.foresight
39
- ```
40
-
41
- ## Which problems does ForesightJS solve?
42
-
43
- ### Problem 1: On-Hover Prefetching Still Has Latency
44
-
45
- Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires.
46
-
47
- ### Problem 2: Viewport-Based Prefetching is Wasteful
48
-
49
- Many modern frameworks (like Next.js) automatically prefetch resources for all links that enter the viewport. While well-intentioned, this creates significant overhead since users typically interact with only a small fraction of visible elements. Simply scrolling up and down the Next.js homepage can trigger **_1.59MB_** of unnecessary prefetch requests.
50
-
51
- ### Problem 3: Hover-Based Prefetching Excludes Keyboard Users
52
-
53
- Many routers rely on hover-based prefetching, but this approach completely excludes keyboard users since keyboard navigation never triggers hover events. This means keyboard users miss out on the performance benefits that mouse users get from hover-based prefetching.
54
-
55
- ### The ForesightJS Solution
56
-
57
- ForesightJS bridges the gap between wasteful viewport prefetching and basic hover prefetching. The `ForesightManager` predicts user interactions by analyzing mouse trajectory patterns, scroll direction and keyboard navigation sequences. This allows you to prefetch resources at the optimal time to improve performance, but targeted enough to avoid waste.
58
-
59
- ## Basic Usage Example
60
-
61
- This basic example is in vanilla JS, ofcourse most people will use ForesightJS with a framework. You can read about framework integrations in the [docs](https://foresightjs.com/docs/integrations).
62
-
63
- ```javascript
64
- import { ForesightManager } from "foresightjs"
65
-
66
- // Initialize the manager if you want custom global settings (do this once at app startup)
67
- ForesightManager.initialize({
68
- // Optional props (see configuration)
69
- })
70
-
71
- // Register an element to be tracked
72
- const myButton = document.getElementById("my-button")
73
-
74
- const { isTouchDevice, unregister } = ForesightManager.instance.register({
75
- element: myButton,
76
- callback: () => {
77
- // This is where your prefetching logic goes
78
- },
79
- hitSlop: 20, // Optional: "hit slop" in pixels. Overwrites defaultHitSlop
80
- // other optional props (see configuration)
81
- })
82
-
83
- // Later, when done with this element:
84
- unregister()
85
- ```
86
-
87
- ## Integrations
88
-
89
- Since ForesightJS is framework agnostic, it can be integrated with any JavaScript framework. While I haven't yet built [integrations](https://foresightjs.com/docs/integrations) for every framework, ready-to-use implementations for [Next.js](https://foresightjs.com/docs/integrations/react/nextjs) and [React Router](https://foresightjs.com/docs/integrations/react/react-router) are already available. Sharing integrations for other frameworks/packages is highly appreciated!
90
-
91
- ## Configuration
92
-
93
- ForesightJS can be used bare-bones but also can be configured. For all configuration possibilities you can reference the [docs](https://foresightjs.com/docs/getting_started/config).
94
-
95
- ## Development Tools
96
-
97
- ForesightJS has dedicated [Development Tools](https://github.com/spaansba/ForesightJS-DevTools) created with [Foresight Events](https://foresightjs.com/docs/getting_started/events) that help you understand and tune how foresight is working in your application. This standalone development package provides real-time visualization of mouse trajectory predictions, element bounds, and callback execution. It's particularly helpful when setting up ForesightJS for the first time or when fine-tuning for specific UI components.
98
-
99
- ```bash
100
- npm install js.foresight-devtools
101
- ```
102
-
103
- See the [development tools documentation](https://foresightjs.com/docs/getting_started/debug) for more details.
104
-
105
- ## What About Touch Devices and Slow Connections?
106
-
107
- Since ForesightJS relies on the keyboard/mouse it will not register elements for touch devices. For limited connections (2G or data-saver mode), we respect the user's preference to minimize data usage and skip registration aswell.
108
-
109
- The `ForesightManager.instance.register()` method returns these properties:
110
-
111
- - `isTouchDevice` - true if user is on a touch device
112
- - `isLimitedConnection` - true when user is on a 2G connection or has data-saver enabled
113
- - `isRegistered` - true if element was actually registered
114
-
115
- With these properties you could create your own fallback prefetching methods if required. For example if the user is on a touch device you could prefetch based on viewport.
116
- An example of this can be found in the [Next.js](https://foresightjs.com/docs/integrations/react/nextjs) or [React Router](https://foresightjs.com/docs/integrations/react/react-router) ForesightLink components.
117
-
118
- ## How Does ForesightJS Work?
119
-
120
- For a detailed technical explanation of its prediction algorithms and internal architecture, see the **[Behind the Scenes documentation](https://foresightjs.com/docs/Behind_the_Scenes)**.
121
-
122
- ## Providing Context to AI Tools
123
-
124
- ForesightJS is a newer library, so most AI assistants and LLMs may not have much built-in knowledge about it. To improve their responses, you can provide the following context:
125
-
126
- - Use [llms.txt](https://foresightjs.com/llms.txt) for a concise overview of the API and usage patterns.
127
- - Use [llms-full.txt](https://foresightjs.com/llms-full.txt) for a full markdown version of the docs, ideal for AI tools that support context injection or uploads.
128
- - All documentation pages are also available in markdown. You can view them by adding .md to the end of any URL, for example: https://foresightjs.com/docs/getting_started.md.
129
-
130
- # Contributing
131
-
132
- Please see the [contributing guidelines](/CONTRIBUTING.md)
1
+ # [ForesightJS](https://foresightjs.com/)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/js.foresight.svg)](https://www.npmjs.com/package/js.foresight)
4
+ [![npm downloads](https://img.shields.io/npm/dt/js.foresight.svg)](https://www.npmjs.com/package/js.foresight)
5
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/js.foresight)](https://bundlephobia.com/package/js.foresight)
6
+ [![GitHub last commit](https://img.shields.io/github/last-commit/spaansba/ForesightJS)](https://github.com/spaansba/ForesightJS/commits)
7
+
8
+ [![GitHub stars](https://img.shields.io/github/stars/spaansba/ForesightJS.svg?style=social&label=Star)](https://github.com/spaansba/ForesightJS)
9
+ [![Best of JS](https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=spaansba%2FForesightJS%26since=daily)](https://bestofjs.org/projects/foresightjs)
10
+
11
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
+ [![Demo](https://img.shields.io/badge/demo-live-blue)](https://foresightjs.com#playground)
14
+ ForesightJS is a lightweight JavaScript library with full TypeScript support that predicts user intent based on mouse movements, scroll and keyboard navigation. By analyzing cursor/scroll trajectory and tab sequences, it anticipates which elements a user is likely to interact with, allowing developers to trigger actions before the actual hover or click occurs (for example prefetching).
15
+
16
+ ### [Playground](https://foresightjs.com/)
17
+
18
+ ![](https://github.com/user-attachments/assets/f5650c63-4489-4878-bd72-d8954c6a739b)
19
+ _In the GIF above, the [ForesightJS DevTools](https://foresightjs.com/docs/getting_started/development_tools) are enabled. Normally, users won't see anything that ForesightJS does except the increased perceived speed from early prefetching._
20
+
21
+ ## Download
22
+
23
+ ```bash
24
+ pnpm add js.foresight
25
+ # or
26
+ npm install js.foresight
27
+ # or
28
+ yarn add js.foresight
29
+ ```
30
+
31
+ ## Which problems does ForesightJS solve?
32
+
33
+ ### Problem 1: On-Hover Prefetching Still Has Latency
34
+
35
+ Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires.
36
+
37
+ ### Problem 2: Viewport-Based Prefetching is Wasteful
38
+
39
+ Many modern frameworks (like Next.js) automatically prefetch resources for all links that enter the viewport. While well-intentioned, this creates significant overhead since users typically interact with only a small fraction of visible elements. Simply scrolling up and down the Next.js homepage can trigger **_1.59MB_** of unnecessary prefetch requests.
40
+
41
+ ### Problem 3: Hover-Based Prefetching Excludes Keyboard Users
42
+
43
+ Many routers rely on hover-based prefetching, but this approach completely excludes keyboard users since keyboard navigation never triggers hover events. This means keyboard users miss out on the performance benefits that mouse users get from hover-based prefetching.
44
+
45
+ ### The ForesightJS Solution
46
+
47
+ ForesightJS bridges the gap between wasteful viewport prefetching and basic hover prefetching. The `ForesightManager` predicts user interactions by analyzing mouse trajectory patterns, scroll direction and keyboard navigation sequences. This allows you to prefetch resources at the optimal time to improve performance, but targeted enough to avoid waste.
48
+
49
+ ## Basic Usage Example
50
+
51
+ This basic example is in vanilla JS, ofcourse most people will use ForesightJS with a framework. You can read about framework integrations in the [docs](https://foresightjs.com/docs/integrations).
52
+
53
+ ```javascript
54
+ import { ForesightManager } from "foresightjs"
55
+
56
+ // Initialize the manager if you want custom global settings (do this once at app startup)
57
+ ForesightManager.initialize({
58
+ // Optional props (see configuration)
59
+ })
60
+
61
+ // Register an element to be tracked
62
+ const myButton = document.getElementById("my-button")
63
+
64
+ const { isTouchDevice, unregister } = ForesightManager.instance.register({
65
+ element: myButton,
66
+ callback: () => {
67
+ // This is where your prefetching logic goes
68
+ },
69
+ hitSlop: 20, // Optional: "hit slop" in pixels. Overwrites defaultHitSlop
70
+ // other optional props (see configuration)
71
+ })
72
+
73
+ // Later, when done with this element:
74
+ unregister()
75
+ ```
76
+
77
+ ## Integrations
78
+
79
+ Since ForesightJS is framework agnostic, it can be integrated with any JavaScript framework. While I haven't yet built [integrations](https://foresightjs.com/docs/integrations) for every framework, ready-to-use implementations for [Next.js](https://foresightjs.com/docs/integrations/react/nextjs) and [React Router](https://foresightjs.com/docs/integrations/react/react-router) are already available. Sharing integrations for other frameworks/packages is highly appreciated!
80
+
81
+ ## Configuration
82
+
83
+ ForesightJS can be used bare-bones but also can be configured. For all configuration possibilities you can reference the [docs](https://foresightjs.com/docs/getting_started/config).
84
+
85
+ ## Development Tools
86
+
87
+ ForesightJS has dedicated [Development Tools](https://github.com/spaansba/ForesightJS-DevTools) created with [Foresight Events](https://foresightjs.com/docs/getting_started/events) that help you understand and tune how foresight is working in your application. This standalone development package provides real-time visualization of mouse trajectory predictions, element bounds, and callback execution. It's particularly helpful when setting up ForesightJS for the first time or when fine-tuning for specific UI components.
88
+
89
+ ```bash
90
+ npm install js.foresight-devtools
91
+ ```
92
+
93
+ See the [development tools documentation](https://foresightjs.com/docs/getting_started/debug) for more details.
94
+
95
+ ## What About Touch Devices and Slow Connections?
96
+
97
+ Since ForesightJS relies on the keyboard/mouse it will not register elements for touch devices. For limited connections (2G or data-saver mode), we respect the user's preference to minimize data usage and skip registration aswell.
98
+
99
+ The `ForesightManager.instance.register()` method returns these properties:
100
+
101
+ - `isTouchDevice` - true if user is on a touch device
102
+ - `isLimitedConnection` - true when user is on a 2G connection or has data-saver enabled
103
+ - `isRegistered` - true if element was actually registered
104
+
105
+ With these properties you could create your own fallback prefetching methods if required. For example if the user is on a touch device you could prefetch based on viewport.
106
+ An example of this can be found in the [Next.js](https://foresightjs.com/docs/integrations/react/nextjs) or [React Router](https://foresightjs.com/docs/integrations/react/react-router) ForesightLink components.
107
+
108
+ ## How Does ForesightJS Work?
109
+
110
+ For a detailed technical explanation of its prediction algorithms and internal architecture, see the **[Behind the Scenes documentation](https://foresightjs.com/docs/Behind_the_Scenes)**.
111
+
112
+ ## Providing Context to AI Tools
113
+
114
+ ForesightJS is a newer library, so most AI assistants and LLMs may not have much built-in knowledge about it. To improve their responses, you can provide the following context:
115
+
116
+ - Use [llms.txt](https://foresightjs.com/llms.txt) for a concise overview of the API and usage patterns.
117
+ - Use [llms-full.txt](https://foresightjs.com/llms-full.txt) for a full markdown version of the docs, ideal for AI tools that support context injection or uploads.
118
+ - All documentation pages are also available in markdown. You can view them by adding .md to the end of any URL, for example: https://foresightjs.com/docs/getting_started.md.
119
+
120
+ # Contributing
121
+
122
+ Please see the [contributing guidelines](/CONTRIBUTING.md)
package/dist/index.d.ts CHANGED
@@ -1,3 +1,22 @@
1
+ declare class CircularBuffer<T> {
2
+ private buffer;
3
+ private head;
4
+ private count;
5
+ private capacity;
6
+ constructor(capacity: number);
7
+ add(item: T): void;
8
+ getFirst(): T | undefined;
9
+ getLast(): T | undefined;
10
+ getFirstLast(): [T | undefined, T | undefined];
11
+ resize(newCapacity: number): void;
12
+ private getAllItems;
13
+ clear(): void;
14
+ get length(): number;
15
+ get size(): number;
16
+ get isFull(): boolean;
17
+ get isEmpty(): boolean;
18
+ }
19
+
1
20
  type Rect = {
2
21
  top: number;
3
22
  left: number;
@@ -29,7 +48,7 @@ type Point = {
29
48
  y: number;
30
49
  };
31
50
  type TrajectoryPositions = {
32
- positions: MousePosition[];
51
+ positions: CircularBuffer<MousePosition>;
33
52
  currentPoint: Point;
34
53
  predictedPoint: Point;
35
54
  };
@@ -95,6 +114,10 @@ type ForesightElementData = Required<Pick<ForesightRegisterOptions, "callback" |
95
114
  * For debugging, check if you are registering the same element multiple times.
96
115
  */
97
116
  registerCount: number;
117
+ /**
118
+ * If set by user, stores additional information about the registered element
119
+ */
120
+ meta: Record<string, unknown>;
98
121
  };
99
122
  type MouseCallbackCounts = {
100
123
  hover: number;
@@ -224,6 +247,10 @@ type ForesightRegisterOptions = {
224
247
  */
225
248
  unregisterOnCallback?: boolean;
226
249
  name?: string;
250
+ /**
251
+ * If set by user, stores additional information about the registered element
252
+ */
253
+ meta?: Record<string, unknown>;
227
254
  };
228
255
  /**
229
256
  * Usefull for if you want to create a custom button component in a modern framework (for example React).
@@ -257,6 +284,7 @@ interface ElementUnregisteredEvent extends ForesightBaseEvent {
257
284
  type: "elementUnregistered";
258
285
  elementData: ForesightElementData;
259
286
  unregisterReason: ElementUnregisteredReason;
287
+ wasLastElement: boolean;
260
288
  }
261
289
  /**
262
290
  * The reason an element was unregistered from ForesightManager's tracking.
@@ -337,21 +365,16 @@ interface ForesightBaseEvent {
337
365
  declare class ForesightManager {
338
366
  private static manager;
339
367
  private elements;
368
+ private trajectoryPositions;
340
369
  private isSetup;
341
370
  private _globalCallbackHits;
342
371
  private _globalSettings;
343
- private trajectoryPositions;
344
- private tabbableElementsCache;
345
- private lastFocusedIndex;
346
- private predictedScrollPoint;
347
- private scrollDirection;
348
372
  private domObserver;
349
373
  private positionObserver;
350
- private lastKeyDown;
351
- private globalListenersController;
352
- private rafId;
353
- private pendingMouseEvent;
354
374
  private eventListeners;
375
+ private mousePredictor;
376
+ private tabPredictor;
377
+ private scrollPredictor;
355
378
  private constructor();
356
379
  static initialize(props?: Partial<UpdateForsightManagerSettings>): ForesightManager;
357
380
  addEventListener<K extends ForesightEvent>(eventType: K, listener: ForesightEventListener<K>, options?: {
@@ -363,15 +386,11 @@ declare class ForesightManager {
363
386
  static get isInitiated(): Readonly<boolean>;
364
387
  static get instance(): ForesightManager;
365
388
  get registeredElements(): ReadonlyMap<ForesightElement, ForesightElementData>;
366
- register({ element, callback, hitSlop, name, }: ForesightRegisterOptions): ForesightRegisterResult;
389
+ register({ element, callback, hitSlop, name, meta, }: ForesightRegisterOptions): ForesightRegisterResult;
367
390
  private unregister;
368
391
  private updateNumericSettings;
369
392
  private updateBooleanSetting;
370
393
  alterGlobalSettings(props?: Partial<UpdateForsightManagerSettings>): void;
371
- private forceUpdateAllElementBounds;
372
- private updatePointerState;
373
- private handleMouseMove;
374
- private processMouseMovement;
375
394
  /**
376
395
  * Detects when registered elements are removed from the DOM and automatically unregisters them to prevent stale references.
377
396
  *
@@ -379,20 +398,18 @@ declare class ForesightManager {
379
398
  *
380
399
  */
381
400
  private handleDomMutations;
382
- private handleKeyDown;
383
- private handleFocusIn;
384
401
  private updateHitCounters;
385
402
  private callCallback;
403
+ private handlePositionChange;
404
+ private handlePositionChangeDataUpdates;
405
+ private initializeGlobalListeners;
406
+ private removeGlobalListeners;
407
+ private forceUpdateAllElementBounds;
386
408
  /**
387
409
  * ONLY use this function when you want to change the rect bounds via code, if the rects are changing because of updates in the DOM do not use this function.
388
410
  * We need an observer for that
389
411
  */
390
412
  private forceUpdateElementBounds;
391
- private handlePositionChange;
392
- private handlePositionChangeDataUpdates;
393
- private handleScrollPrefetch;
394
- private initializeGlobalListeners;
395
- private removeGlobalListeners;
396
413
  }
397
414
 
398
415
  export { type CallbackCompletedEvent, type CallbackHitType, type CallbackHits, type CallbackInvokedEvent, type ElementDataUpdatedEvent, type ElementRegisteredEvent, type ElementUnregisteredEvent, type ForesightElement, type ForesightElementData, type ForesightEvent, ForesightManager, type ForesightManagerSettings, type Rect as ForesightRect, type ForesightRegisterOptions, type ForesightRegisterOptionsWithoutElement, type ForesightRegisterResult, type HitSlop, type ManagerSettingsChangedEvent, type MouseTrajectoryUpdateEvent, type ScrollTrajectoryUpdateEvent, type UpdateForsightManagerSettings, type UpdatedManagerSetting };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var E=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var B=(i,e)=>{for(var t in e)E(i,t,{get:e[t],enumerable:!0})},K=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of k(e))!j.call(i,o)&&o!==t&&E(i,o,{get:()=>e[o],enumerable:!(n=U(e,o))||n.enumerable});return i};var G=i=>K(E({},"__esModule",{value:!0}),i);var ae={};B(ae,{ForesightManager:()=>f});module.exports=G(ae);var x=require("tabbable");function P(){let i=Y(),e=z();return{isTouchDevice:i,isLimitedConnection:e,shouldRegister:!i&&!e}}function Y(){return window.matchMedia("(pointer: coarse)").matches&&navigator.maxTouchPoints>0}function z(){let i=navigator.connection;return i?/2g/.test(i.effectiveType)||i.saveData:!1}function d(i,e,t,n){return i<e?console.warn(`ForesightJS: "${n}" value ${i} is below minimum bound ${e}, clamping to ${e}`):i>t&&console.warn(`ForesightJS: "${n}" value ${i} is above maximum bound ${t}, clamping to ${t}`),Math.min(Math.max(i,e),t)}function S(i,e,t){let n=0,o=1,s=e.x-i.x,r=e.y-i.y,a=(l,u)=>{if(l===0){if(u<0)return!1}else{let c=u/l;if(l<0){if(c>o)return!1;c>n&&(n=c)}else{if(c<n)return!1;c<o&&(o=c)}}return!0};return!a(-s,i.x-t.left)||!a(s,t.right-i.x)||!a(-r,i.y-t.top)||!a(r,t.bottom-i.y)?!1:n<=o}function _(i,e,t,n){let o=performance.now(),s={point:i,time:o},{x:r,y:a}=i;if(e.push(s),e.length>t&&e.shift(),e.length<2)return{x:r,y:a};let l=e[0],u=e[e.length-1],c=(u.time-l.time)/1e3;if(c===0)return{x:r,y:a};let g=u.point.x-l.point.x,L=u.point.y-l.point.y,N=g/c,A=L/c,R=n/1e3,H=r+N*R,w=a+A*R;return{x:H,y:w}}function v(i){if(typeof i=="number"){let e=d(i,0,2e3,"hitslop");return{top:e,left:e,right:e,bottom:e}}return{top:d(i.top,0,2e3,"hitslop - top"),left:d(i.left,0,2e3,"hitslop - left"),right:d(i.right,0,2e3,"hitslop - right"),bottom:d(i.bottom,0,2e3,"hitslop - bottom")}}function m(i,e){return{left:i.left-e.left,right:i.right+e.right,top:i.top-e.top,bottom:i.bottom+e.bottom}}function T(i,e){return!i||!e?i===e:i.left===e.left&&i.right===e.right&&i.top===e.top&&i.bottom===e.bottom}function y(i,e){return i.x>=e.left&&i.x<=e.right&&i.y>=e.top&&i.y<=e.bottom}function I(i,e){return i!==void 0&&e!==i}function M(i,e,t,n){if(e!==null){let o=i?e-1:e+1;if(o>=0&&o<t.length&&t[o]===n)return o}return t.findIndex(o=>o===n)}function F(i,e){let n=e.top-i.top,o=e.left-i.left;return n<-1?"down":n>1?"up":o<-1?"right":o>1?"left":"none"}function C(i,e,t){let{x:n,y:o}=i,s={x:n,y:o};switch(e){case"down":s.y+=t;break;case"up":s.y-=t;break;case"left":s.x-=t;break;case"right":s.x+=t;break;case"none":break;default:}return s}var D=require("position-observer");function O(i){let e=window.innerWidth||document.documentElement.clientWidth,t=window.innerHeight||document.documentElement.clientHeight;return i.top<t&&i.bottom>0&&i.left<e&&i.right>0}var f=class i{constructor(){this.elements=new Map;this.isSetup=!1;this._globalCallbackHits={mouse:{hover:0,trajectory:0},tab:{forwards:0,reverse:0},scroll:{down:0,left:0,right:0,up:0},total:0};this._globalSettings={debug:!1,enableMousePrediction:!0,enableScrollPrediction:!0,positionHistorySize:8,trajectoryPredictionTime:120,scrollMargin:150,defaultHitSlop:{top:0,left:0,right:0,bottom:0},enableTabPrediction:!0,tabOffset:2};this.trajectoryPositions={positions:[],currentPoint:{x:0,y:0},predictedPoint:{x:0,y:0}};this.tabbableElementsCache=[];this.lastFocusedIndex=null;this.predictedScrollPoint=null;this.scrollDirection=null;this.domObserver=null;this.positionObserver=null;this.lastKeyDown=null;this.globalListenersController=new AbortController;this.rafId=null;this.pendingMouseEvent=null;this.eventListeners=new Map;this.handleMouseMove=e=>{this.pendingMouseEvent=e,!this.rafId&&(this.rafId=requestAnimationFrame(()=>{this.pendingMouseEvent&&(this.processMouseMovement(this.pendingMouseEvent),this.pendingMouseEvent=null),this.rafId=null}))};this.handleDomMutations=e=>{e.length&&(this.tabbableElementsCache=[],this.lastFocusedIndex=null);for(let t of e)if(t.type==="childList"&&t.removedNodes.length>0)for(let n of this.elements.keys())n.isConnected||this.unregister(n,"disconnected")};this.handleKeyDown=e=>{e.key==="Tab"&&(this.lastKeyDown=e)};this.handleFocusIn=e=>{if(!this.lastKeyDown||!this._globalSettings.enableTabPrediction)return;let t=e.target;if(!(t instanceof HTMLElement))return;this.tabbableElementsCache.length||(this.tabbableElementsCache=(0,x.tabbable)(document.documentElement));let n=this.lastKeyDown.shiftKey,o=M(n,this.lastFocusedIndex,this.tabbableElementsCache,t);this.lastFocusedIndex=o,this.lastKeyDown=null;let s=[];for(let r=0;r<=this._globalSettings.tabOffset;r++)if(n){let a=this.tabbableElementsCache[o-r];this.elements.has(a)&&s.push(a)}else{let a=this.tabbableElementsCache[o+r];this.elements.has(a)&&s.push(a)}s.forEach(r=>{let a=this.elements.get(r);a&&this.callCallback(a,{kind:"tab",subType:n?"reverse":"forwards"})})};this.handlePositionChange=e=>{for(let t of e){let n=this.elements.get(t.target);n&&(this.handlePositionChangeDataUpdates(n,t),this.handleScrollPrefetch(n,t.boundingClientRect))}this.scrollDirection=null,this.predictedScrollPoint=null};this.handlePositionChangeDataUpdates=(e,t)=>{let n=[],o=t.isIntersecting,s={...e,isIntersectingWithViewport:o};e.isIntersectingWithViewport!==o&&n.push("visibility"),o&&(n.push("bounds"),this.handleScrollPrefetch(s,t.boundingClientRect),s={...s,elementBounds:{hitSlop:e.elementBounds.hitSlop,originalRect:t.boundingClientRect,expandedRect:m(t.boundingClientRect,e.elementBounds.hitSlop)}}),this.elements.set(e.element,s),n.length&&this.emit({type:"elementDataUpdated",elementData:s,updatedProps:n})}}static initialize(e){return this.isInitiated||(i.manager=new i),e!==void 0&&i.manager.alterGlobalSettings(e),i.manager}addEventListener(e,t,n){if(n?.signal?.aborted)return()=>{};let o=this.eventListeners.get(e)??[];o.push(t),this.eventListeners.set(e,o),n?.signal?.addEventListener("abort",()=>this.removeEventListener(e,t))}removeEventListener(e,t){let n=this.eventListeners.get(e);if(!n)return;let o=n.indexOf(t);o>-1&&n.splice(o,1)}emit(e){let t=this.eventListeners.get(e.type);t&&t.forEach(n=>{try{n(e)}catch(o){console.error(`Error in ForesightManager event listener for ${e.type}:`,o)}})}get getManagerData(){return{registeredElements:this.elements,globalSettings:this._globalSettings,globalCallbackHits:this._globalCallbackHits,eventListeners:this.eventListeners}}static get isInitiated(){return!!i.manager}static get instance(){return this.initialize()}get registeredElements(){return this.elements}register({element:e,callback:t,hitSlop:n,name:o}){let{shouldRegister:s,isTouchDevice:r,isLimitedConnection:a}=P();if(!s)return{isLimitedConnection:a,isTouchDevice:r,isRegistered:!1,unregister:()=>{}};this.isSetup||this.initializeGlobalListeners();let l=e.getBoundingClientRect(),u=n?v(n):this._globalSettings.defaultHitSlop,c={element:e,callback:t,elementBounds:{originalRect:l,expandedRect:m(l,u),hitSlop:u},isHovering:!1,trajectoryHitData:{isTrajectoryHit:!1,trajectoryHitTime:0,trajectoryHitExpirationTimeoutId:void 0},name:o||e.id||"unnamed",isIntersectingWithViewport:O(l),isRunningCallback:!1,registerCount:(this.registeredElements.get(e)?.registerCount??0)+1};return this.elements.set(e,c),this.positionObserver?.observe(e),this.emit({type:"elementRegistered",timestamp:Date.now(),elementData:c}),{isTouchDevice:r,isLimitedConnection:a,isRegistered:!0,unregister:()=>this.unregister(e,"apiCall")}}unregister(e,t){if(!this.elements.has(e))return;let n=this.elements.get(e);n?.trajectoryHitData.trajectoryHitExpirationTimeoutId&&clearTimeout(n.trajectoryHitData.trajectoryHitExpirationTimeoutId),this.positionObserver?.unobserve(e),this.elements.delete(e),this.elements.size===0&&this.isSetup&&this.removeGlobalListeners(),n&&this.emit({type:"elementUnregistered",elementData:n,timestamp:Date.now(),unregisterReason:t})}updateNumericSettings(e,t,n,o){return I(e,this._globalSettings[t])?(this._globalSettings[t]=d(e,n,o,t),!0):!1}updateBooleanSetting(e,t){return I(e,this._globalSettings[t])?(this._globalSettings[t]=e,!0):!1}alterGlobalSettings(e){let t=[];if([{setting:"positionHistorySize",min:2,max:30},{setting:"trajectoryPredictionTime",min:10,max:200},{setting:"scrollMargin",min:30,max:300},{setting:"tabOffset",min:0,max:20}].forEach(({setting:s,min:r,max:a})=>{let l=e?.[s];if(l===void 0)return;let u=this._globalSettings[s];if(this.updateNumericSettings(l,s,r,a)&&(t.push({setting:s,oldValue:u,newValue:this._globalSettings[s]}),s==="positionHistorySize"&&this._globalSettings.positionHistorySize<u)){let g=this._globalSettings.positionHistorySize;this.trajectoryPositions.positions.length>g&&(this.trajectoryPositions.positions=this.trajectoryPositions.positions.slice(-g))}}),["enableMousePrediction","enableScrollPrediction","enableTabPrediction"].forEach(s=>{let r=e?.[s];if(r===void 0)return;let a=this._globalSettings[s];this.updateBooleanSetting(r,s)&&t.push({setting:s,oldValue:a,newValue:this._globalSettings[s]})}),e?.defaultHitSlop!==void 0){let s=this._globalSettings.defaultHitSlop,r=v(e.defaultHitSlop);T(s,r)||(this._globalSettings.defaultHitSlop=r,t.push({setting:"defaultHitSlop",oldValue:s,newValue:r}),this.forceUpdateAllElementBounds())}t.length>0&&this.emit({type:"managerSettingsChanged",timestamp:Date.now(),managerData:this.getManagerData,updatedSettings:t})}forceUpdateAllElementBounds(){for(let[e,t]of this.elements)t.isIntersectingWithViewport&&this.forceUpdateElementBounds(t)}updatePointerState(e){let t={x:e.clientX,y:e.clientY};this.trajectoryPositions.currentPoint=t,this._globalSettings.enableMousePrediction?this.trajectoryPositions.predictedPoint=_(t,this.trajectoryPositions.positions,this._globalSettings.positionHistorySize,this._globalSettings.trajectoryPredictionTime):this.trajectoryPositions.predictedPoint=t}processMouseMovement(e){this.updatePointerState(e);for(let t of this.elements.values()){if(!t.isIntersectingWithViewport)continue;let n=t.elementBounds.expandedRect;if(this._globalSettings.enableMousePrediction)S(this.trajectoryPositions.currentPoint,this.trajectoryPositions.predictedPoint,n)&&this.callCallback(t,{kind:"mouse",subType:"trajectory"});else if(y(this.trajectoryPositions.currentPoint,n)){this.callCallback(t,{kind:"mouse",subType:"hover"});return}}this.emit({type:"mouseTrajectoryUpdate",predictionEnabled:this._globalSettings.enableMousePrediction,trajectoryPositions:this.trajectoryPositions})}updateHitCounters(e,t){switch(e.kind){case"mouse":this._globalCallbackHits.mouse[e.subType]++;break;case"tab":this._globalCallbackHits.tab[e.subType]++;break;case"scroll":this._globalCallbackHits.scroll[e.subType]++;break;default:}this._globalCallbackHits.total++}callCallback(e,t){if(e.isRunningCallback)return;e.isRunningCallback=!0,(async()=>{this.updateHitCounters(t,e),this.emit({type:"callbackInvoked",timestamp:Date.now(),elementData:e,hitType:t});let o=performance.now();try{await e.callback(),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:e,hitType:t,elapsed:performance.now()-o,status:"success"})}catch(s){let r=s instanceof Error?s.message:String(s);console.error(`Error in callback for element ${e.name} (${e.element.tagName}):`,s),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:e,hitType:t,elapsed:performance.now()-o,status:"error",errorMessage:r})}this.unregister(e.element,"callbackHit")})()}forceUpdateElementBounds(e){let t=e.element.getBoundingClientRect(),n=m(t,e.elementBounds.hitSlop);if(!T(n,e.elementBounds.expandedRect)){let o={...e,elementBounds:{...e.elementBounds,originalRect:t,expandedRect:n}};this.elements.set(e.element,o),this.emit({type:"elementDataUpdated",elementData:o,updatedProps:["bounds"]})}}handleScrollPrefetch(e,t){if(e.isIntersectingWithViewport)if(!this._globalSettings.enableScrollPrediction)y(this.trajectoryPositions.currentPoint,e.elementBounds.expandedRect)&&this.callCallback(e,{kind:"mouse",subType:"hover"});else{if(this.scrollDirection=this.scrollDirection??F(e.elementBounds.originalRect,t),this.scrollDirection==="none")return;this.predictedScrollPoint=this.predictedScrollPoint??C(this.trajectoryPositions.currentPoint,this.scrollDirection,this._globalSettings.scrollMargin),S(this.trajectoryPositions.currentPoint,this.predictedScrollPoint,e.elementBounds.expandedRect)&&this.callCallback(e,{kind:"scroll",subType:this.scrollDirection}),this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.predictedScrollPoint,scrollDirection:this.scrollDirection})}}initializeGlobalListeners(){if(this.isSetup||typeof window>"u"||typeof document>"u")return;let{signal:e}=this.globalListenersController;document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("keydown",this.handleKeyDown,{signal:e}),document.addEventListener("focusin",this.handleFocusIn,{signal:e}),this.domObserver=new MutationObserver(this.handleDomMutations),this.domObserver.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1}),this.positionObserver=new D.PositionObserver(this.handlePositionChange),this.isSetup=!0}removeGlobalListeners(){this.isSetup=!1,this.globalListenersController?.abort(),this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.domObserver?.disconnect(),this.domObserver=null,this.positionObserver?.disconnect(),this.positionObserver=null,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.pendingMouseEvent=null}};0&&(module.exports={ForesightManager});
1
+ import{PositionObserver as rt}from"position-observer";var y=class{constructor(t){this.head=0;this.count=0;if(t<=0)throw new Error("CircularBuffer capacity must be greater than 0");this.capacity=t,this.buffer=new Array(t)}add(t){this.buffer[this.head]=t,this.head=(this.head+1)%this.capacity,this.count<this.capacity&&this.count++}getFirst(){if(this.count!==0)return this.count<this.capacity?this.buffer[0]:this.buffer[this.head]}getLast(){if(this.count!==0){if(this.count<this.capacity)return this.buffer[this.count-1];{let t=(this.head-1+this.capacity)%this.capacity;return this.buffer[t]}}}getFirstLast(){if(this.count===0)return[void 0,void 0];if(this.count===1){let i=this.count<this.capacity?this.buffer[0]:this.buffer[this.head];return[i,i]}let t=this.getFirst(),e=this.getLast();return[t,e]}resize(t){if(t<=0)throw new Error("CircularBuffer capacity must be greater than 0");if(t===this.capacity)return;let e=this.getAllItems();if(this.capacity=t,this.buffer=new Array(t),this.head=0,this.count=0,e.length>t){let i=e.slice(-t);for(let n of i)this.add(n)}else for(let i of e)this.add(i)}getAllItems(){if(this.count===0)return[];let t=new Array(this.count);if(this.count<this.capacity)for(let e=0;e<this.count;e++)t[e]=this.buffer[e];else{let e=this.head;for(let i=0;i<this.capacity;i++){let n=(e+i)%this.capacity;t[i]=this.buffer[n]}}return t}clear(){this.head=0,this.count=0}get length(){return this.count}get size(){return this.capacity}get isFull(){return this.count===this.capacity}get isEmpty(){return this.count===0}};function u(o,t,e,i){return o<t?console.warn(`ForesightJS: "${i}" value ${o} is below minimum bound ${t}, clamping to ${t}`):o>e&&console.warn(`ForesightJS: "${i}" value ${o} is above maximum bound ${e}, clamping to ${e}`),Math.min(Math.max(o,t),e)}function j(o){let t=window.innerWidth||document.documentElement.clientWidth,e=window.innerHeight||document.documentElement.clientHeight;return o.top<e&&o.bottom>0&&o.left<t&&o.right>0}function F(o){if(typeof o=="number"){let t=u(o,0,2e3,"hitslop");return{top:t,left:t,right:t,bottom:t}}return{top:u(o.top,0,2e3,"hitslop - top"),left:u(o.left,0,2e3,"hitslop - left"),right:u(o.right,0,2e3,"hitslop - right"),bottom:u(o.bottom,0,2e3,"hitslop - bottom")}}function E(o,t){return{left:o.left-t.left,right:o.right+t.right,top:o.top-t.top,bottom:o.bottom+t.bottom}}function x(o,t){return!o||!t?o===t:o.left===t.left&&o.right===t.right&&o.top===t.top&&o.bottom===t.bottom}function S(o,t){return o.x>=t.left&&o.x<=t.right&&o.y>=t.top&&o.y<=t.bottom}function H(){let o=z(),t=K();return{isTouchDevice:o,isLimitedConnection:t,shouldRegister:!o&&!t}}function z(){return window.matchMedia("(pointer: coarse)").matches&&navigator.maxTouchPoints>0}function K(){let o=navigator.connection;return o?/2g/.test(o.effectiveType)||o.saveData:!1}function O(o,t){return o!==void 0&&t!==o}function T(o,t,e){let i=0,n=1,r=t.x-o.x,c=t.y-o.y,s=(a,l)=>{if(a===0){if(l<0)return!1}else{let d=l/a;if(a<0){if(d>n)return!1;d>i&&(i=d)}else{if(d<i)return!1;d<n&&(n=d)}}return!0};return!s(-r,o.x-e.left)||!s(r,e.right-o.x)||!s(-c,o.y-e.top)||!s(c,e.bottom-o.y)?!1:i<=n}function N(o,t,e){let i=performance.now(),n={point:o,time:i},{x:r,y:c}=o;if(t.add(n),t.length<2)return{x:r,y:c};let[s,a]=t.getFirstLast();if(!s||!a)return{x:r,y:c};let l=(a.time-s.time)*.001;if(l===0)return{x:r,y:c};let d=a.point.x-s.point.x,p=a.point.y-s.point.y,L=d/l,C=p/l,I=e*.001,R=r+L*I,w=c+C*I;return{x:R,y:w}}var h=class{constructor(t){this.elements=t.dependencies.elements,this.callCallback=t.dependencies.callCallback,this.emit=t.dependencies.emit,this.abortController=new AbortController}abort(){this.abortController.abort()}handleError(t,e){console.error(`${this.constructor.name} error in ${e}:`,t)}};var v=class extends h{constructor(e){super(e);this.pendingMouseEvent=null;this.rafId=null;this.handleMouseMove=e=>{this.pendingMouseEvent=e,!this.rafId&&(this.rafId=requestAnimationFrame(()=>{this.pendingMouseEvent&&this.processMouseMovement(this.pendingMouseEvent),this.rafId=null}))};this.enableMousePrediction=e.settings.enableMousePrediction,this.trajectoryPredictionTime=e.settings.trajectoryPredictionTime,this.positionHistorySize=e.settings.positionHistorySize,this.trajectoryPositions=e.trajectoryPositions,this.initializeListeners()}initializeListeners(){let{signal:e}=this.abortController;document.addEventListener("mousemove",this.handleMouseMove,{signal:e})}updatePointerState(e){let i={x:e.clientX,y:e.clientY};this.trajectoryPositions.currentPoint=i,this.enableMousePrediction?this.trajectoryPositions.predictedPoint=N(i,this.trajectoryPositions.positions,this.trajectoryPredictionTime):this.trajectoryPositions.predictedPoint=i}cleanup(){this.abort(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.pendingMouseEvent=null}processMouseMovement(e){try{this.updatePointerState(e);for(let i of this.elements.values()){if(!i.isIntersectingWithViewport)continue;let n=i.elementBounds.expandedRect;if(this.enableMousePrediction)T(this.trajectoryPositions.currentPoint,this.trajectoryPositions.predictedPoint,n)&&this.callCallback(i,{kind:"mouse",subType:"trajectory"});else if(S(this.trajectoryPositions.currentPoint,n)){this.callCallback(i,{kind:"mouse",subType:"hover"});return}}this.emit({type:"mouseTrajectoryUpdate",predictionEnabled:this.enableMousePrediction,trajectoryPositions:this.trajectoryPositions})}catch(i){this.handleError(i,"processMouseMovement")}}};function A(o,t){let i=t.top-o.top,n=t.left-o.left;return i<-1?"down":i>1?"up":n<-1?"right":n>1?"left":"none"}function k(o,t,e){let{x:i,y:n}=o,r={x:i,y:n};switch(t){case"down":r.y+=e;break;case"up":r.y-=e;break;case"left":r.x-=e;break;case"right":r.x+=e;break;case"none":break;default:}return r}var f=class extends h{constructor(e){super(e);this.predictedScrollPoint=null;this.scrollDirection=null;this.scrollMargin=e.settings.scrollMargin,this.trajectoryPositions=e.trajectoryPositions}initializeListeners(){}cleanup(){this.abort(),this.resetScrollProps()}resetScrollProps(){this.scrollDirection=null,this.predictedScrollPoint=null}handleScrollPrefetch(e,i){if(e.isIntersectingWithViewport)try{if(this.scrollDirection=this.scrollDirection??A(e.elementBounds.originalRect,i),this.scrollDirection==="none")return;this.predictedScrollPoint=this.predictedScrollPoint??k(this.trajectoryPositions.currentPoint,this.scrollDirection,this.scrollMargin),T(this.trajectoryPositions.currentPoint,this.predictedScrollPoint,e.elementBounds.expandedRect)&&this.callCallback(e,{kind:"scroll",subType:this.scrollDirection}),this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.predictedScrollPoint,scrollDirection:this.scrollDirection})}catch(n){this.handleError(n,"handleScrollPrefetch")}}handleError(e,i){super.handleError(e,i),this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.trajectoryPositions.currentPoint,scrollDirection:"none"})}};import{tabbable as V}from"tabbable";function U(o,t,e,i){if(t!==null&&t>-1){let n=o?t-1:t+1;if(n>=0&&n<e.length&&e[n]===i)return n}return e.findIndex(n=>n===i)}var P=class extends h{constructor(e){super(e);this.lastKeyDown=null;this.tabbableElementsCache=[];this.lastFocusedIndex=null;this.handleKeyDown=e=>{e.key==="Tab"&&(this.lastKeyDown=e)};this.handleFocusIn=e=>{try{if(!this.lastKeyDown)return;let i=e.target;if(!(i instanceof HTMLElement))return;(!this.tabbableElementsCache.length||this.lastFocusedIndex===-1)&&(this.tabbableElementsCache=V(document.documentElement));let n=this.lastKeyDown.shiftKey,r=U(n,this.lastFocusedIndex,this.tabbableElementsCache,i);this.lastFocusedIndex=r,this.lastKeyDown=null;let c=[];for(let s=0;s<=this.tabOffset;s++){let a=n?r-s:r+s,l=this.tabbableElementsCache[a];l&&l instanceof Element&&this.elements.has(l)&&c.push(l)}c.forEach(s=>{let a=this.elements.get(s);a&&this.callCallback(a,{kind:"tab",subType:n?"reverse":"forwards"})})}catch(i){this.handleError(i,"handleFocusIn")}};this.tabOffset=e.settings.tabOffset,this.initializeListeners()}initializeListeners(){let{signal:e}=this.abortController;document.addEventListener("keydown",this.handleKeyDown,{signal:e}),document.addEventListener("focusin",this.handleFocusIn,{signal:e})}invalidateCache(){this.tabbableElementsCache=[],this.lastFocusedIndex=null}cleanup(){this.abort(),this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.lastKeyDown=null}};var D=class o{constructor(){this.elements=new Map;this.trajectoryPositions={positions:new y(8),currentPoint:{x:0,y:0},predictedPoint:{x:0,y:0}};this.isSetup=!1;this._globalCallbackHits={mouse:{hover:0,trajectory:0},tab:{forwards:0,reverse:0},scroll:{down:0,left:0,right:0,up:0},total:0};this._globalSettings={debug:!1,enableMousePrediction:!0,enableScrollPrediction:!0,positionHistorySize:8,trajectoryPredictionTime:120,scrollMargin:150,defaultHitSlop:{top:0,left:0,right:0,bottom:0},enableTabPrediction:!0,tabOffset:2};this.domObserver=null;this.positionObserver=null;this.eventListeners=new Map;this.mousePredictor=null;this.tabPredictor=null;this.scrollPredictor=null;this.handleDomMutations=t=>{t.length&&this.tabPredictor?.invalidateCache();for(let e of t)if(e.type==="childList"&&e.removedNodes.length>0)for(let i of this.elements.keys())i.isConnected||this.unregister(i,"disconnected")};this.handlePositionChange=t=>{for(let e of t){let i=this.elements.get(e.target);i&&(this._globalSettings.enableScrollPrediction?this.scrollPredictor?.handleScrollPrefetch(i,e.boundingClientRect):S(this.trajectoryPositions.currentPoint,i.elementBounds.expandedRect)&&this.callCallback(i,{kind:"mouse",subType:"hover"}),this.handlePositionChangeDataUpdates(i,e))}this._globalSettings.enableScrollPrediction&&this.scrollPredictor?.resetScrollProps()};this.handlePositionChangeDataUpdates=(t,e)=>{let i=[],n=e.isIntersecting;t.isIntersectingWithViewport!==n&&(i.push("visibility"),t.isIntersectingWithViewport=n),n&&(i.push("bounds"),t.elementBounds={hitSlop:t.elementBounds.hitSlop,originalRect:e.boundingClientRect,expandedRect:E(e.boundingClientRect,t.elementBounds.hitSlop)}),i.length&&this.emit({type:"elementDataUpdated",elementData:t,updatedProps:i})}}static initialize(t){return this.isInitiated||(o.manager=new o),t!==void 0&&o.manager.alterGlobalSettings(t),o.manager}addEventListener(t,e,i){if(i?.signal?.aborted)return()=>{};let n=this.eventListeners.get(t)??[];n.push(e),this.eventListeners.set(t,n),i?.signal?.addEventListener("abort",()=>this.removeEventListener(t,e))}removeEventListener(t,e){let i=this.eventListeners.get(t);if(!i)return;let n=i.indexOf(e);n>-1&&i.splice(n,1)}emit(t){let e=this.eventListeners.get(t.type);e&&e.forEach((i,n)=>{try{i(t)}catch(r){console.error(`Error in ForesightManager event listener ${n} for ${t.type}:`,r)}})}get getManagerData(){return{registeredElements:this.elements,globalSettings:this._globalSettings,globalCallbackHits:this._globalCallbackHits,eventListeners:this.eventListeners}}static get isInitiated(){return!!o.manager}static get instance(){return this.initialize()}get registeredElements(){return this.elements}register({element:t,callback:e,hitSlop:i,name:n,meta:r}){let{shouldRegister:c,isTouchDevice:s,isLimitedConnection:a}=H();if(!c)return{isLimitedConnection:a,isTouchDevice:s,isRegistered:!1,unregister:()=>{}};this.isSetup||this.initializeGlobalListeners();let l=t.getBoundingClientRect(),d=i?F(i):this._globalSettings.defaultHitSlop,p={element:t,callback:e,elementBounds:{originalRect:l,expandedRect:E(l,d),hitSlop:d},isHovering:!1,trajectoryHitData:{isTrajectoryHit:!1,trajectoryHitTime:0,trajectoryHitExpirationTimeoutId:void 0},name:n||t.id||"unnamed",isIntersectingWithViewport:j(l),isRunningCallback:!1,registerCount:(this.registeredElements.get(t)?.registerCount??0)+1,meta:r??{}};return this.elements.set(t,p),this.positionObserver?.observe(t),this.emit({type:"elementRegistered",timestamp:Date.now(),elementData:p}),{isTouchDevice:s,isLimitedConnection:a,isRegistered:!0,unregister:()=>this.unregister(t,"apiCall")}}unregister(t,e){if(!this.elements.has(t))return;let i=this.elements.get(t);i?.trajectoryHitData.trajectoryHitExpirationTimeoutId&&clearTimeout(i.trajectoryHitData.trajectoryHitExpirationTimeoutId),this.positionObserver?.unobserve(t),this.elements.delete(t);let n=this.elements.size===0&&this.isSetup;n&&this.removeGlobalListeners(),i&&this.emit({type:"elementUnregistered",elementData:i,timestamp:Date.now(),unregisterReason:e,wasLastElement:n})}updateNumericSettings(t,e,i,n){return O(t,this._globalSettings[e])?(this._globalSettings[e]=u(t,i,n,e),!0):!1}updateBooleanSetting(t,e){return O(t,this._globalSettings[e])?(this._globalSettings[e]=t,!0):!1}alterGlobalSettings(t){let e=[],i=this._globalSettings.trajectoryPredictionTime;this.updateNumericSettings(t?.trajectoryPredictionTime,"trajectoryPredictionTime",10,200)&&e.push({setting:"trajectoryPredictionTime",oldValue:i,newValue:this._globalSettings.trajectoryPredictionTime});let r=this._globalSettings.positionHistorySize;this.updateNumericSettings(t?.positionHistorySize,"positionHistorySize",2,30)&&(e.push({setting:"positionHistorySize",oldValue:r,newValue:this._globalSettings.positionHistorySize}),this.trajectoryPositions.positions.resize(this._globalSettings.positionHistorySize));let s=this._globalSettings.scrollMargin;if(this.updateNumericSettings(t?.scrollMargin,"scrollMargin",30,300)){let g=this._globalSettings.scrollMargin;this.scrollPredictor&&(this.scrollPredictor.scrollMargin=g),e.push({setting:"scrollMargin",oldValue:s,newValue:g})}let l=this._globalSettings.tabOffset;this.updateNumericSettings(t?.tabOffset,"tabOffset",0,20)&&(this.tabPredictor&&(this.tabPredictor.tabOffset=this._globalSettings.tabOffset),e.push({setting:"tabOffset",oldValue:l,newValue:this._globalSettings.tabOffset}));let p=this._globalSettings.enableMousePrediction;this.updateBooleanSetting(t?.enableMousePrediction,"enableMousePrediction")&&e.push({setting:"enableMousePrediction",oldValue:p,newValue:this._globalSettings.enableMousePrediction});let C=this._globalSettings.enableScrollPrediction;this.updateBooleanSetting(t?.enableScrollPrediction,"enableScrollPrediction")&&(this._globalSettings.enableScrollPrediction?this.scrollPredictor=new f({dependencies:{callCallback:this.callCallback.bind(this),emit:this.emit.bind(this),elements:this.elements},settings:{scrollMargin:this._globalSettings.scrollMargin},trajectoryPositions:this.trajectoryPositions}):(this.scrollPredictor?.cleanup(),this.scrollPredictor=null),e.push({setting:"enableScrollPrediction",oldValue:C,newValue:this._globalSettings.enableScrollPrediction}));let R=this._globalSettings.enableTabPrediction;if(this.updateBooleanSetting(t?.enableTabPrediction,"enableTabPrediction")&&(e.push({setting:"enableTabPrediction",oldValue:R,newValue:this._globalSettings.enableTabPrediction}),this._globalSettings.enableTabPrediction?this.tabPredictor=new P({dependencies:{callCallback:this.callCallback.bind(this),emit:this.emit.bind(this),elements:this.elements},settings:{tabOffset:this._globalSettings.tabOffset}}):(this.tabPredictor?.cleanup(),this.tabPredictor=null)),t?.defaultHitSlop!==void 0){let g=this._globalSettings.defaultHitSlop,_=F(t.defaultHitSlop);x(g,_)||(this._globalSettings.defaultHitSlop=_,e.push({setting:"defaultHitSlop",oldValue:g,newValue:_}),this.forceUpdateAllElementBounds())}e.length>0&&this.emit({type:"managerSettingsChanged",timestamp:Date.now(),managerData:this.getManagerData,updatedSettings:e})}updateHitCounters(t){switch(t.kind){case"mouse":this._globalCallbackHits.mouse[t.subType]++;break;case"tab":this._globalCallbackHits.tab[t.subType]++;break;case"scroll":this._globalCallbackHits.scroll[t.subType]++;break;default:}this._globalCallbackHits.total++}callCallback(t,e){if(t.isRunningCallback)return;let i=async()=>{this.updateHitCounters(e),this.emit({type:"callbackInvoked",timestamp:Date.now(),elementData:t,hitType:e});let n=performance.now();try{await t.callback(),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:t,hitType:e,elapsed:performance.now()-n,status:"success"})}catch(r){let c=r instanceof Error?r.message:String(r);console.error(`Error in callback for element ${t.name} (${t.element.tagName}):`,r),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:t,hitType:e,elapsed:performance.now()-n,status:"error",errorMessage:c})}this.unregister(t.element,"callbackHit")};t.isRunningCallback=!0,i()}initializeGlobalListeners(){if(this.isSetup||typeof window>"u"||typeof document>"u")return;let t=this._globalSettings,e={callCallback:this.callCallback.bind(this),emit:this.emit.bind(this),elements:this.elements};t.enableTabPrediction&&(this.tabPredictor=new P({dependencies:e,settings:{tabOffset:t.tabOffset}})),t.enableScrollPrediction&&(this.scrollPredictor=new f({dependencies:e,settings:{scrollMargin:t.scrollMargin},trajectoryPositions:this.trajectoryPositions})),this.mousePredictor=new v({dependencies:e,settings:{enableMousePrediction:t.enableMousePrediction,trajectoryPredictionTime:t.trajectoryPredictionTime,positionHistorySize:t.positionHistorySize},trajectoryPositions:this.trajectoryPositions}),this.domObserver=new MutationObserver(this.handleDomMutations),this.domObserver.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1}),this.positionObserver=new rt(this.handlePositionChange),this.isSetup=!0}removeGlobalListeners(){this.isSetup=!1,this.domObserver?.disconnect(),this.domObserver=null,this.positionObserver?.disconnect(),this.positionObserver=null,this.mousePredictor?.cleanup(),this.tabPredictor?.cleanup(),this.scrollPredictor?.cleanup()}forceUpdateAllElementBounds(){for(let[,t]of this.elements)t.isIntersectingWithViewport&&this.forceUpdateElementBounds(t)}forceUpdateElementBounds(t){let e=t.element.getBoundingClientRect(),i=E(e,t.elementBounds.hitSlop);if(!x(i,t.elementBounds.expandedRect)){let n={...t,elementBounds:{...t.elementBounds,originalRect:e,expandedRect:i}};this.elements.set(t.element,n),this.emit({type:"elementDataUpdated",elementData:n,updatedProps:["bounds"]})}}};export{D as ForesightManager};
2
2
  //# sourceMappingURL=index.js.map