js.foresight 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ You supply the **What** and **How** inside your `callback` when you register an
26
26
  ### [Playground](https://foresightjs.com/)
27
27
 
28
28
  ![](https://github.com/user-attachments/assets/36c81a82-fee7-43d6-ba1e-c48214136f90)
29
- _In the GIF above, [debug mode](https://foresightjs.com/docs/getting_started/debug) is on. Normally, users won't see anything that ForesightJS does except the increased perceived speed from early prefetching._
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
30
 
31
31
  ## Download
32
32
 
@@ -64,10 +64,8 @@ This basic example is in vanilla JS, ofcourse most people will use ForesightJS w
64
64
  import { ForesightManager } from "foresightjs"
65
65
 
66
66
  // Initialize the manager if you want custom global settings (do this once at app startup)
67
- // If you dont want global settings, you dont have to initialize the manager
68
67
  ForesightManager.initialize({
69
- debug: false, // Set to true to see visualization
70
- trajectoryPredictionTime: 80, // How far ahead (in milliseconds) to predict the mouse trajectory
68
+ // Optional props (see configuration)
71
69
  })
72
70
 
73
71
  // Register an element to be tracked
@@ -79,6 +77,7 @@ const { isTouchDevice, unregister } = ForesightManager.instance.register({
79
77
  // This is where your prefetching logic goes
80
78
  },
81
79
  hitSlop: 20, // Optional: "hit slop" in pixels. Overwrites defaultHitSlop
80
+ // other optional props (see configuration)
82
81
  })
83
82
 
84
83
  // Later, when done with this element:
@@ -93,9 +92,15 @@ Since ForesightJS is framework agnostic, it can be integrated with any JavaScrip
93
92
 
94
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).
95
94
 
96
- ## Debugging Visualization
95
+ ## Development Tools
97
96
 
98
- ForesightJS includes a [Visual Debugging](https://foresightjs.com/docs/getting_started/debug) system that helps you understand and tune how foresight is working in your application. This is particularly helpful when setting up ForesightJS for the first time or when fine-tuning for specific UI components.
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.
99
104
 
100
105
  ## What About Touch Devices and Slow Connections?
101
106
 
@@ -120,16 +125,6 @@ Since ForesightJS is a relatively new and unknown library, most AI assistants an
120
125
 
121
126
  Additionally, every page in the documentation is available in markdown format (try adding .md to any documentation URL). You can share these markdown files as context with AI assistants, though all this information is also consolidated in the llms.txt file for convenience.
122
127
 
123
- ## Future of ForesightJS
124
-
125
- ForesightJS will continue to evolve with a focus on staying as lightweight and performant as possible. To achieve this the plan is to decouple the debugger and make it its own standalone dev package, reducing the core library size even further.
126
-
127
- Beyond size optimization, performance remains central to every development decision. Each release will focus on improving prediction accuracy while reducing computational overhead, ensuring ForesightJS stays practical for production environments. We also want to move as much processing as possible off the main thread to keep user interfaces responsive.
128
-
129
- These performance improvements go hand in hand with expanding accessibility across different development environments. The documentation will grow to include more framework integrations beyond the current Next.js and React Router implementations, making ForesightJS accessible to developers working with different technology stacks and routing solutions.
130
-
131
- All of these efforts benefit from community input. [Contributions](/CONTRIBUTING.md) are always welcome, whether for new framework integrations, performance improvements, or feature ideas.
132
-
133
128
  # Contributing
134
129
 
135
130
  Please see the [contributing guidelines](/CONTRIBUTING.md)
@@ -0,0 +1,398 @@
1
+ type Rect = {
2
+ top: number;
3
+ left: number;
4
+ right: number;
5
+ bottom: number;
6
+ };
7
+ /**
8
+ * A callback function that is executed when a foresight interaction
9
+ * (e.g., hover, trajectory hit) occurs on a registered element.
10
+ */
11
+ type ForesightCallback = () => void;
12
+ /**
13
+ * Represents the HTML element that is being tracked by the ForesightManager.
14
+ * This is typically a standard DOM `Element`.
15
+ */
16
+ type ForesightElement = Element;
17
+ /**
18
+ * Represents a mouse position captured at a specific point in time.
19
+ * Used for tracking mouse movement history.
20
+ */
21
+ type MousePosition = {
22
+ /** The (x, y) coordinates of the mouse. */
23
+ point: Point;
24
+ /** The timestamp (e.g., from `performance.now()`) when this position was recorded. */
25
+ time: number;
26
+ };
27
+ type Point = {
28
+ x: number;
29
+ y: number;
30
+ };
31
+ type TrajectoryPositions = {
32
+ positions: MousePosition[];
33
+ currentPoint: Point;
34
+ predictedPoint: Point;
35
+ };
36
+ /**
37
+ * Internal type representing the calculated boundaries of a foresight element,
38
+ * including its original dimensions and the expanded hit area.
39
+ */
40
+ type ElementBounds = {
41
+ /** The expanded rectangle, including hitSlop, used for interaction detection. */
42
+ expandedRect: Readonly<Rect>;
43
+ /** The original bounding rectangle of the element, as returned by `getBoundingClientRect()`. */
44
+ originalRect: DOMRectReadOnly;
45
+ /** The hit slop values applied to this element. */
46
+ hitSlop: Exclude<HitSlop, number>;
47
+ };
48
+ /**
49
+ * Represents trajectory hit related data for a foresight element.
50
+ */
51
+ type TrajectoryHitData = {
52
+ /** True if the predicted mouse trajectory has intersected the element's expanded bounds. */
53
+ isTrajectoryHit: boolean;
54
+ /** The timestamp when the last trajectory hit occurred. */
55
+ trajectoryHitTime: number;
56
+ /** Timeout ID for expiring the trajectory hit state. */
57
+ trajectoryHitExpirationTimeoutId?: ReturnType<typeof setTimeout>;
58
+ };
59
+ type ForesightRegisterResult = {
60
+ /** Whether the current device is a touch device. This is important as ForesightJS only works based on cursor movement. If the user is using a touch device you should handle prefetching differently */
61
+ isTouchDevice: boolean;
62
+ /** Whether the user has connection limitations (slow network (2g) or data saver enabled) that should prevent prefetching */
63
+ isLimitedConnection: boolean;
64
+ /** Whether ForesightJS will actively track this element. False if touch device or limited connection, true otherwise */
65
+ isRegistered: boolean;
66
+ /** Function to unregister the element */
67
+ unregister: () => void;
68
+ };
69
+ /**
70
+ * Represents the data associated with a registered foresight element.
71
+ */
72
+ type ForesightElementData = Required<Pick<ForesightRegisterOptions, "callback" | "name">> & {
73
+ /** The boundary information for the element. */
74
+ elementBounds: ElementBounds;
75
+ /** True if the mouse cursor is currently hovering over the element's expanded bounds. */
76
+ isHovering: boolean;
77
+ /**
78
+ * Represents trajectory hit related data for a foresight element. Only used for the manager
79
+ */
80
+ trajectoryHitData: TrajectoryHitData;
81
+ /**
82
+ * Is the element intersecting with the viewport, usefull to track which element we should observe or not
83
+ * Can be @undefined in the split second the element is registering
84
+ */
85
+ isIntersectingWithViewport: boolean;
86
+ /**
87
+ * The element you registered
88
+ */
89
+ element: ForesightElement;
90
+ /**
91
+ * If the element is currently running its callback
92
+ */
93
+ isRunningCallback: boolean;
94
+ /**
95
+ * For debugging, check if you are registering the same element multiple times.
96
+ */
97
+ registerCount: number;
98
+ };
99
+ type MouseCallbackCounts = {
100
+ hover: number;
101
+ trajectory: number;
102
+ };
103
+ type TabCallbackCounts = {
104
+ reverse: number;
105
+ forwards: number;
106
+ };
107
+ type ScrollDirection = "down" | "up" | "left" | "right" | "none";
108
+ type ScrollCallbackCounts = Record<`${Exclude<ScrollDirection, "none">}`, number>;
109
+ type CallbackHits = {
110
+ total: number;
111
+ mouse: MouseCallbackCounts;
112
+ tab: TabCallbackCounts;
113
+ scroll: ScrollCallbackCounts;
114
+ };
115
+ type CallbackHitType = {
116
+ kind: "mouse";
117
+ subType: keyof MouseCallbackCounts;
118
+ } | {
119
+ kind: "tab";
120
+ subType: keyof TabCallbackCounts;
121
+ } | {
122
+ kind: "scroll";
123
+ subType: keyof ScrollCallbackCounts;
124
+ };
125
+ /**
126
+ * Snapshot of the current ForesightManager state
127
+ */
128
+ type ForesightManagerData = {
129
+ registeredElements: ReadonlyMap<ForesightElement, ForesightElementData>;
130
+ globalSettings: Readonly<ForesightManagerSettings>;
131
+ globalCallbackHits: Readonly<CallbackHits>;
132
+ eventListeners: ReadonlyMap<keyof ForesightEventMap, ForesightEventListener[]>;
133
+ };
134
+ type BaseForesightManagerSettings = {
135
+ /**
136
+ * Number of mouse positions to keep in history for trajectory calculation.
137
+ * A higher number might lead to smoother but slightly delayed predictions.
138
+ *
139
+ *
140
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
141
+ *
142
+ *
143
+ * **This value is clamped between 2 and 30.**
144
+ * @default 8
145
+ */
146
+ positionHistorySize: number;
147
+ /**
148
+ *
149
+ * @deprecated will be removed from v4.0
150
+ * ForesightJS now have its stand-alone devtools library with the debugger built-in
151
+ * @link https://github.com/spaansba/ForesightJS-DevTools
152
+ */
153
+ debug: boolean;
154
+ /**
155
+ * How far ahead (in milliseconds) to predict the mouse trajectory.
156
+ * A larger value means the prediction extends further into the future. (meaning it will trigger callbacks sooner)
157
+ *
158
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
159
+ *
160
+ * **This value is clamped between 10 and 200.**
161
+ * @default 120
162
+ */
163
+ trajectoryPredictionTime: number;
164
+ /**
165
+ * Whether to enable mouse trajectory prediction.
166
+ * If false, only direct hover/interaction is considered.
167
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
168
+ * @default true
169
+ */
170
+ enableMousePrediction: boolean;
171
+ /**
172
+ * Toggles whether keyboard prediction is on
173
+ *
174
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
175
+ * @default true
176
+ */
177
+ enableTabPrediction: boolean;
178
+ /**
179
+ * Sets the pixel distance to check from the mouse position in the scroll direction.
180
+ *
181
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
182
+ *
183
+ * **This value is clamped between 30 and 300.**
184
+ * @default 150
185
+ */
186
+ scrollMargin: number;
187
+ /**
188
+ * Toggles whether scroll prediction is on
189
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
190
+ * @default true
191
+ */
192
+ enableScrollPrediction: boolean;
193
+ /**
194
+ * Tab stops away from an element to trigger callback. Only works when @argument enableTabPrediction is true
195
+ *
196
+ * **This value is clamped between 0 and 20.**
197
+ * @default 2
198
+ */
199
+ tabOffset: number;
200
+ };
201
+ /**
202
+ * Configuration options for the ForesightManager
203
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
204
+ */
205
+ type ForesightManagerSettings = BaseForesightManagerSettings & {
206
+ defaultHitSlop: Exclude<HitSlop, number>;
207
+ };
208
+ /**
209
+ * Update options for the ForesightManager
210
+ * @link https://foresightjs.com/docs/getting_started/config#available-global-settings
211
+ */
212
+ type UpdateForsightManagerSettings = BaseForesightManagerSettings & {
213
+ defaultHitSlop: HitSlop;
214
+ };
215
+ /**
216
+ * Type used to register elements to the foresight manager
217
+ */
218
+ type ForesightRegisterOptions = {
219
+ element: ForesightElement;
220
+ callback: ForesightCallback;
221
+ hitSlop?: HitSlop;
222
+ /**
223
+ * @deprecated will be removed in V4.0
224
+ */
225
+ unregisterOnCallback?: boolean;
226
+ name?: string;
227
+ };
228
+ /**
229
+ * Usefull for if you want to create a custom button component in a modern framework (for example React).
230
+ * And you want to have the ForesightRegisterOptions used in ForesightManager.instance.register({})
231
+ * without the element as the element will be the ref of the component.
232
+ *
233
+ * @link https://foresightjs.com/docs/getting_started/typescript#foresightregisteroptionswithoutelement
234
+ */
235
+ type ForesightRegisterOptionsWithoutElement = Omit<ForesightRegisterOptions, "element">;
236
+ /**
237
+ * Fully invisible "slop" around the element.
238
+ * Basically increases the hover hitbox
239
+ */
240
+ type HitSlop = Rect | number;
241
+ interface ForesightEventMap {
242
+ elementRegistered: ElementRegisteredEvent;
243
+ elementUnregistered: ElementUnregisteredEvent;
244
+ elementDataUpdated: ElementDataUpdatedEvent;
245
+ callbackInvoked: CallbackInvokedEvent;
246
+ callbackCompleted: CallbackCompletedEvent;
247
+ mouseTrajectoryUpdate: MouseTrajectoryUpdateEvent;
248
+ scrollTrajectoryUpdate: ScrollTrajectoryUpdateEvent;
249
+ managerSettingsChanged: ManagerSettingsChangedEvent;
250
+ }
251
+ type ForesightEvent = "elementRegistered" | "elementUnregistered" | "elementDataUpdated" | "callbackInvoked" | "callbackCompleted" | "mouseTrajectoryUpdate" | "scrollTrajectoryUpdate" | "managerSettingsChanged";
252
+ interface ElementRegisteredEvent extends ForesightBaseEvent {
253
+ type: "elementRegistered";
254
+ elementData: ForesightElementData;
255
+ }
256
+ interface ElementUnregisteredEvent extends ForesightBaseEvent {
257
+ type: "elementUnregistered";
258
+ elementData: ForesightElementData;
259
+ unregisterReason: ElementUnregisteredReason;
260
+ }
261
+ /**
262
+ * The reason an element was unregistered from ForesightManager's tracking.
263
+ * - `callbackHit`: The element was automatically unregistered after its callback fired.
264
+ * - `disconnected`: The element was automatically unregistered because it was removed from the DOM.
265
+ * - `apiCall`: The developer manually called the `unregister()` function for the element.
266
+ */
267
+ type ElementUnregisteredReason = "callbackHit" | "disconnected" | "apiCall";
268
+ interface ElementDataUpdatedEvent extends Omit<ForesightBaseEvent, "timestamp"> {
269
+ type: "elementDataUpdated";
270
+ elementData: ForesightElementData;
271
+ updatedProps: UpdatedDataPropertyNames[];
272
+ }
273
+ type UpdatedDataPropertyNames = "bounds" | "visibility";
274
+ interface CallbackInvokedEvent extends ForesightBaseEvent {
275
+ type: "callbackInvoked";
276
+ elementData: ForesightElementData;
277
+ hitType: CallbackHitType;
278
+ }
279
+ interface CallbackCompletedEventBase extends ForesightBaseEvent {
280
+ type: "callbackCompleted";
281
+ elementData: ForesightElementData;
282
+ hitType: CallbackHitType;
283
+ elapsed: number;
284
+ }
285
+ type CallbackCompletedEvent = CallbackCompletedEventBase & ({
286
+ status: "success";
287
+ } | {
288
+ status: "error";
289
+ errorMessage: string;
290
+ });
291
+ interface MouseTrajectoryUpdateEvent extends Omit<ForesightBaseEvent, "timestamp"> {
292
+ type: "mouseTrajectoryUpdate";
293
+ trajectoryPositions: TrajectoryPositions;
294
+ predictionEnabled: boolean;
295
+ }
296
+ interface ScrollTrajectoryUpdateEvent extends Omit<ForesightBaseEvent, "timestamp"> {
297
+ type: "scrollTrajectoryUpdate";
298
+ currentPoint: Point;
299
+ predictedPoint: Point;
300
+ scrollDirection: ScrollDirection;
301
+ }
302
+ interface ManagerSettingsChangedEvent extends ForesightBaseEvent {
303
+ type: "managerSettingsChanged";
304
+ managerData: ForesightManagerData;
305
+ updatedSettings: UpdatedManagerSetting[];
306
+ }
307
+ type UpdatedManagerSetting = {
308
+ [K in keyof ForesightManagerSettings]: {
309
+ setting: K;
310
+ newValue: ForesightManagerSettings[K];
311
+ oldValue: ForesightManagerSettings[K];
312
+ };
313
+ }[keyof ForesightManagerSettings];
314
+ type ForesightEventListener<K extends ForesightEvent = ForesightEvent> = (event: ForesightEventMap[K]) => void;
315
+ interface ForesightBaseEvent {
316
+ type: ForesightEvent;
317
+ timestamp: number;
318
+ }
319
+
320
+ /**
321
+ * Manages the prediction of user intent based on mouse trajectory and element interactions.
322
+ *
323
+ * ForesightManager is a singleton class responsible for:
324
+ * - Registering HTML elements to monitor.
325
+ * - Tracking mouse movements and predicting future cursor positions.
326
+ * - Detecting when a predicted trajectory intersects with a registered element's bounds.
327
+ * - Invoking callbacks associated with elements upon predicted or actual interaction.
328
+ * - Optionally unregistering elements after their callback is triggered.
329
+ * - Handling global settings for prediction behavior (e.g., history size, prediction time).
330
+ * - Automatically updating element bounds on resize using {@link ResizeObserver}.
331
+ * - Automatically unregistering elements removed from the DOM using {@link MutationObserver}.
332
+ * - Detecting broader layout shifts via {@link MutationObserver} to update element positions.
333
+ *
334
+ * It should be initialized once using {@link ForesightManager.initialize} and then
335
+ * accessed via the static getter {@link ForesightManager.instance}.
336
+ */
337
+ declare class ForesightManager {
338
+ private static manager;
339
+ private elements;
340
+ private isSetup;
341
+ private _globalCallbackHits;
342
+ private _globalSettings;
343
+ private trajectoryPositions;
344
+ private tabbableElementsCache;
345
+ private lastFocusedIndex;
346
+ private predictedScrollPoint;
347
+ private scrollDirection;
348
+ private domObserver;
349
+ private positionObserver;
350
+ private lastKeyDown;
351
+ private globalListenersController;
352
+ private rafId;
353
+ private pendingMouseEvent;
354
+ private eventListeners;
355
+ private constructor();
356
+ static initialize(props?: Partial<UpdateForsightManagerSettings>): ForesightManager;
357
+ addEventListener<K extends ForesightEvent>(eventType: K, listener: ForesightEventListener<K>, options?: {
358
+ signal?: AbortSignal;
359
+ }): (() => void) | undefined;
360
+ removeEventListener<K extends ForesightEvent>(eventType: K, listener: ForesightEventListener<K>): void;
361
+ private emit;
362
+ get getManagerData(): Readonly<ForesightManagerData>;
363
+ static get isInitiated(): Readonly<boolean>;
364
+ static get instance(): ForesightManager;
365
+ get registeredElements(): ReadonlyMap<ForesightElement, ForesightElementData>;
366
+ register({ element, callback, hitSlop, name, }: ForesightRegisterOptions): ForesightRegisterResult;
367
+ private unregister;
368
+ private updateNumericSettings;
369
+ private updateBooleanSetting;
370
+ alterGlobalSettings(props?: Partial<UpdateForsightManagerSettings>): void;
371
+ private forceUpdateAllElementBounds;
372
+ private updatePointerState;
373
+ private handleMouseMove;
374
+ private processMouseMovement;
375
+ /**
376
+ * Detects when registered elements are removed from the DOM and automatically unregisters them to prevent stale references.
377
+ *
378
+ * @param mutationsList - Array of MutationRecord objects describing the DOM changes
379
+ *
380
+ */
381
+ private handleDomMutations;
382
+ private handleKeyDown;
383
+ private handleFocusIn;
384
+ private updateHitCounters;
385
+ private callCallback;
386
+ /**
387
+ * 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
+ * We need an observer for that
389
+ */
390
+ private forceUpdateElementBounds;
391
+ private handlePositionChange;
392
+ private handlePositionChangeDataUpdates;
393
+ private handleScrollPrefetch;
394
+ private initializeGlobalListeners;
395
+ private removeGlobalListeners;
396
+ }
397
+
398
+ 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 };