Behind the Scenes
Reading this is not necessary to use the library; this is just for understanding how ForesightJS works.
This document delves into the internal workings of ForesightJS, offering a look "behind the scenes" at its architecture and prediction logic. While understanding these details isn't necessary for using the library, it provides insight into how ForesightJS manages elements, observes changes, and predicts user interactions like mouse movements and tab navigation. The following sections explore the core ForesightManager
, the observers it employs, and the algorithms behind its predictive capabilities.
ForesightManager Structure
ForesightJS employs a singleton pattern, ensuring only one instance of ForesightManager
exists. This central instance manages all prediction logic, registered elements, and global settings. Elements are stored in a Map
, where the key is the registered element itself. Calling ForesightManager.instance.register()
multiple times with the same element overwrites the existing entry rather than creating a duplicate.
To keep the DOM clean and optimize for performance,
General Observers
ForesightJS utilizes browser-native Observers to monitor element positions and their presence in the DOM:
MutationObserver
: This observer detects when registered HTML elements are removed from the DOM, leading to their automatic unregistration. Not needed but helpful for if the user forgets to unregister their element on removal.
-
PositionObserver
: This observer, Created by Shopify, is used to asynchronously observe changes in the position of a registered element. Its callback provides a list of all detected changes, allowing us to consolidate multiple observer patterns. By implementing thePositionObserver
, we avoid the need for:- The window resize event
- The window scroll event
- The ResizeObserver
- The IntersectionObserver
Mouse Prediction
Event Handlers
For accurate mouse predictions, ForesightJS captures mouse movements next to the previously mentioned layout changes:
mousemove
: The library records the mouse'sclientX
andclientY
coordinates on eachmousemove
event. ForesightJS maintains a history of these positions, the size of which is limited by thepositionHistorySize
setting.
Mouse Position Prediction Mechanism
ForesightJS predicts the mouse's future location using linear extrapolation based on its recent movement history.
The predictNextMousePosition
function implements this in three main steps:
- History Tracking: The function utilizes stored past mouse positions, each with an associated timestamp.
- Velocity Calculation: It calculates the average velocity (change in position over time) using the oldest and newest points in the recorded history.
- Extrapolation: Using this calculated velocity and the
trajectoryPredictionTimeInMs
setting (which defines how far into the future to predict), the function projects the mouse's current position along its path to estimate its futurex
andy
coordinates.
This process yields a predictedPoint
. ForesightJS then creates a line in memory between the current mouse position and this predictedPoint
for intersection checks. It only checks elements that are currently visible with the IntersectionObserver
.
Trajectory Intersection Checking
To determine if the predicted mouse path will intersect with a registered element, ForesightJS employs the lineSegmentIntersectsRect
function. This function implements the Liang-Barsky line clipping algorithm, an efficient method for checking intersections between a line segment (the predicted mouse path) and a rectangular area (the expanded bounds of a registered element).
The process involves:
- Defining the Line Segment: The line segment is defined by the
currentPoint
(current mouse position) and thepredictedPoint
(from the extrapolation step). - Defining the Rectangle: The target rectangle is the
expandedRect
of a registered element, which includes itshitSlop
. - Clipping Tests: The algorithm iteratively "clips" the line segment against each of the rectangle's four edges, calculating if and where the line segment crosses each edge.
- Intersection Determination: An intersection is confirmed if a valid portion of the line segment remains after all clipping tests (specifically, if its calculated entry parameter (t_0) is less than or equal to its exit parameter (t_1)).
This mechanism allows ForesightJS to predict if the user's future mouse trajectory will intersect an element. If an intersection is detected, the element's registered callback
function is invoked.
Tab Prediction
Event Handlers
For tab prediction, ForesightJS monitors specific keyboard and focus events:
keydown
: The library listens forkeydown
events to check if theTab
key was the last key pressed.focusin
: This event fires when an element gains focus. When afocusin
event follows aTab
key press detected viakeydown
, ForesightJS identifies this sequence as tab navigation.
These event listeners are managed by an AbortController
for proper cleanup.
Tab Navigation Prediction
When ForesightJS detects a Tab
key press followed by a focusin
event, it recognizes this as user-initiated tab navigation. Identifying all currently tabbable elements on a page is a surprisingly complex task due to varying browser behaviors, element states, and accessibility considerations. Therefore, to reliably determine the tab order, ForesightJS leverages the robust and well-tested tabbable
library.
The prediction logic then proceeds as follows:
- Identify Current Focus: ForesightJS determines the index of the newly focused element within the ordered list of all tabbable elements provided by the
tabbable
library. - Determine Tab Direction: The library checks if the
Shift
key was held during theTab
press to ascertain the tabbing direction (forward or backward). - Calculate Prediction Range: Based on the current focused element's index, the tab direction, and the configured
tabOffset
, ForesightJS defines a range of elements that are likely to receive focus next. - Trigger Callbacks: If any registered ForesightJS elements fall within this predicted range, their respective
callback
functions are invoked.
Scroll Prediction
You might think we use an event handler for this, but we actually don't have to. If you remember how the PositionObserver
functions, you might know where this is going. As mentioned before, the PositionObserver
callback returns an array of elements that have been moved in some way. By checking the delta between the previous boundingClientRect
and the new one for an element that stayed in the viewport, we can determine which direction the element is moving and thus determine the scroll direction.
Note that this approach might also trigger in other scenarios where elements move, such as animations or dynamic layout changes. If your website has a lot of these, you might need to turn off Scroll Prediction all together.