js.foresight
Version:
Predicts mouse trajectory to trigger actions as users approach elements, enabling anticipatory UI updates or pre-loading. Made with vanilla javascript and usable in every framework.
385 lines (382 loc) • 15.1 kB
TypeScript
type Rect = {
top: number;
left: number;
right: number;
bottom: number;
};
/**
* A callback function that is executed when a foresight interaction
* (e.g., hover, trajectory hit) occurs on a registered element.
*/
type ForesightCallback = () => void;
/**
* Represents the HTML element that is being tracked by the ForesightManager.
* This is typically a standard DOM `Element`.
*/
type ForesightElement = Element;
/**
* Represents a mouse position captured at a specific point in time.
* Used for tracking mouse movement history.
*/
type MousePosition = {
/** The (x, y) coordinates of the mouse. */
point: Point;
/** The timestamp (e.g., from `performance.now()`) when this position was recorded. */
time: number;
};
type Point = {
x: number;
y: number;
};
type TrajectoryPositions = {
positions: MousePosition[];
currentPoint: Point;
predictedPoint: Point;
};
/**
* Internal type representing the calculated boundaries of a foresight element,
* including its original dimensions and the expanded hit area.
*/
type ElementBounds = {
/** The expanded rectangle, including hitSlop, used for interaction detection. */
expandedRect: Rect;
/** The original bounding rectangle of the element, as returned by `getBoundingClientRect()`. */
originalRect?: DOMRectReadOnly | undefined;
/** The hit slop values applied to this element. */
hitSlop: Exclude<HitSlop, number>;
};
/**
* Represents trajectory hit related data for a foresight element.
*/
type TrajectoryHitData = {
/** True if the predicted mouse trajectory has intersected the element's expanded bounds. */
isTrajectoryHit: boolean;
/** The timestamp when the last trajectory hit occurred. */
trajectoryHitTime: number;
/** Timeout ID for expiring the trajectory hit state. */
trajectoryHitExpirationTimeoutId?: ReturnType<typeof setTimeout>;
};
type ForesightRegisterResult = {
/** 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 */
isTouchDevice: boolean;
/** Whether the user has connection limitations (slow network (2g) or data saver enabled) that should prevent prefetching */
isLimitedConnection: boolean;
/** Whether ForesightJS will actively track this element. False if touch device or limited connection, true otherwise */
isRegistered: boolean;
/** Function to unregister the element */
unregister: () => void;
};
/**
* Represents the data associated with a registered foresight element.
*/
type ForesightElementData = Required<Pick<ForesightRegisterOptions, "callback" | "name">> & {
/** The boundary information for the element. */
elementBounds: ElementBounds;
/** True if the mouse cursor is currently hovering over the element's expanded bounds. */
isHovering: boolean;
/**
* Represents trajectory hit related data for a foresight element. Only used for the manager
*/
trajectoryHitData: TrajectoryHitData;
/**
* Is the element intersecting with the viewport, usefull to track which element we should observe or not
*/
isIntersectingWithViewport: boolean;
/**
* The element you registered
*/
element: ForesightElement;
};
type MouseCallbackCounts = {
hover: number;
trajectory: number;
};
type TabCallbackCounts = {
reverse: number;
forwards: number;
};
type ScrollDirection = "down" | "up" | "left" | "right" | "none";
type ScrollCallbackCounts = Record<`${Exclude<ScrollDirection, "none">}`, number>;
type CallbackHits = {
total: number;
mouse: MouseCallbackCounts;
tab: TabCallbackCounts;
scroll: ScrollCallbackCounts;
};
type HitType = {
kind: "mouse";
subType: keyof MouseCallbackCounts;
} | {
kind: "tab";
subType: keyof TabCallbackCounts;
} | {
kind: "scroll";
subType: keyof ScrollCallbackCounts;
};
/**
* Snapshot of the current ForesightManager state
*/
type ForesightManagerData = {
registeredElements: ReadonlyMap<ForesightElement, ForesightElementData>;
globalSettings: Readonly<ForesightManagerSettings>;
globalCallbackHits: Readonly<CallbackHits>;
};
type BaseForesightManagerSettings = {
/**
* Number of mouse positions to keep in history for trajectory calculation.
* A higher number might lead to smoother but slightly delayed predictions.
*
*
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
*
*
* **This value is clamped between 2 and 30.**
* @default 8
*/
positionHistorySize: number;
/**
*
* @deprecated will be removed from v4.0
* ForesightJS now have its stand-alone devtools library with the debugger built-in
* @link https://github.com/spaansba/ForesightJS-DevTools
*/
debug: boolean;
/**
* How far ahead (in milliseconds) to predict the mouse trajectory.
* A larger value means the prediction extends further into the future. (meaning it will trigger callbacks sooner)
*
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
*
* **This value is clamped between 10 and 200.**
* @default 120
*/
trajectoryPredictionTime: number;
/**
* Whether to enable mouse trajectory prediction.
* If false, only direct hover/interaction is considered.
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
* @default true
*/
enableMousePrediction: boolean;
/**
* Toggles whether keyboard prediction is on
*
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
* @default true
*/
enableTabPrediction: boolean;
/**
* Sets the pixel distance to check from the mouse position in the scroll direction.
*
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
*
* **This value is clamped between 30 and 300.**
* @default 150
*/
scrollMargin: number;
/**
* Toggles whether scroll prediction is on
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
* @default true
*/
enableScrollPrediction: boolean;
/**
* Tab stops away from an element to trigger callback. Only works when @argument enableTabPrediction is true
*
* **This value is clamped between 0 and 20.**
* @default 2
*/
tabOffset: number;
/**
* A global callback that runs whenever a callback is fired for any
* registered element, just after the element's specific callback is fired.
*
* @param elementData - The ForesightTarget object for the element that triggered the event.
* @param managerData - Data about the ForesightManager
*/
onAnyCallbackFired: (elementData: ForesightElementData, managerData: ForesightManagerData) => void;
};
/**
* Configuration options for the ForesightManager
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
*/
type ForesightManagerSettings = BaseForesightManagerSettings & {
defaultHitSlop: Exclude<HitSlop, number>;
};
/**
* Update options for the ForesightManager
* @link https://foresightjs.com/docs/getting_started/config#available-global-settings
*/
type UpdateForsightManagerSettings = BaseForesightManagerSettings & {
defaultHitSlop: HitSlop;
};
/**
* Type used to register elements to the foresight manager
*/
type ForesightRegisterOptions = {
element: ForesightElement;
callback: ForesightCallback;
hitSlop?: HitSlop;
/**
* @deprecated will be removed in V4.0
*/
unregisterOnCallback?: boolean;
name?: string;
};
/**
* Usefull for if you want to create a custom button component in a modern framework (for example React).
* And you want to have the ForesightRegisterOptions used in ForesightManager.instance.register({})
* without the element as the element will be the ref of the component.
*
* @link https://foresightjs.com/docs/getting_started/typescript#foresightregisteroptionswithoutelement
*/
type ForesightRegisterOptionsWithoutElement = Omit<ForesightRegisterOptions, "element">;
/**
* Fully invisible "slop" around the element.
* Basically increases the hover hitbox
*/
type HitSlop = Rect | number;
interface ForesightEventMap {
elementRegistered: ElementRegisteredEvent;
elementUnregistered: ElementUnregisteredEvent;
elementDataUpdated: ElementDataUpdatedEvent;
callbackFired: CallbackFiredEvent;
mouseTrajectoryUpdate: MouseTrajectoryUpdateEvent;
scrollTrajectoryUpdate: ScrollTrajectoryUpdateEvent;
managerSettingsChanged: ManagerSettingsChangedEvent;
}
type ForesightEventType = keyof ForesightEventMap;
interface ForesightEvent {
type: ForesightEventType;
timestamp: number;
}
interface ElementRegisteredEvent extends ForesightEvent {
type: "elementRegistered";
elementData: ForesightElementData;
}
interface ElementUnregisteredEvent extends ForesightEvent {
type: "elementUnregistered";
elementData: ForesightElementData;
unregisterReason: ElementUnregisteredReason;
}
/**
* The reason an element was unregistered from ForesightManager's tracking.
* - `callbackHit`: The element was automatically unregistered after its callback fired.
* - `disconnected`: The element was automatically unregistered because it was removed from the DOM.
* - `apiCall`: The developer manually called the `unregister()` function for the element.
*/
type ElementUnregisteredReason = "callbackHit" | "disconnected" | "apiCall";
interface ElementDataUpdatedEvent extends ForesightEvent {
type: "elementDataUpdated";
elementData: ForesightElementData;
updatedProp: "bounds" | "visibility";
}
interface CallbackFiredEvent extends ForesightEvent {
type: "callbackFired";
elementData: ForesightElementData;
hitType: HitType;
managerData: ForesightManagerData;
}
interface MouseTrajectoryUpdateEvent extends ForesightEvent {
type: "mouseTrajectoryUpdate";
trajectoryPositions: TrajectoryPositions;
predictionEnabled: boolean;
}
interface ScrollTrajectoryUpdateEvent extends ForesightEvent {
type: "scrollTrajectoryUpdate";
currentPoint: Point;
predictedPoint: Point;
}
interface ManagerSettingsChangedEvent extends ForesightEvent {
type: "managerSettingsChanged";
managerData: ForesightManagerData;
}
/**
* Manages the prediction of user intent based on mouse trajectory and element interactions.
*
* ForesightManager is a singleton class responsible for:
* - Registering HTML elements to monitor.
* - Tracking mouse movements and predicting future cursor positions.
* - Detecting when a predicted trajectory intersects with a registered element's bounds.
* - Invoking callbacks associated with elements upon predicted or actual interaction.
* - Optionally unregistering elements after their callback is triggered.
* - Handling global settings for prediction behavior (e.g., history size, prediction time).
* - Automatically updating element bounds on resize using {@link ResizeObserver}.
* - Automatically unregistering elements removed from the DOM using {@link MutationObserver}.
* - Detecting broader layout shifts via {@link MutationObserver} to update element positions.
*
* It should be initialized once using {@link ForesightManager.initialize} and then
* accessed via the static getter {@link ForesightManager.instance}.
*/
declare class ForesightManager {
private static manager;
private elements;
private isSetup;
private _globalCallbackHits;
private _globalSettings;
private trajectoryPositions;
private tabbableElementsCache;
private lastFocusedIndex;
private predictedScrollPoint;
private scrollDirection;
private domObserver;
private positionObserver;
private lastKeyDown;
private globalListenersController;
private eventListeners;
private constructor();
static initialize(props?: Partial<UpdateForsightManagerSettings>): ForesightManager;
addEventListener<K extends ForesightEventType>(eventType: K, listener: (event: ForesightEventMap[K]) => void, options?: {
signal?: AbortSignal;
}): (() => void) | undefined;
removeEventListener<K extends ForesightEventType>(eventType: K, listener: (event: ForesightEventMap[K]) => void): void;
logSubscribers(): void;
private emit;
get getManagerData(): Readonly<ForesightManagerData>;
static get isInitiated(): Readonly<boolean>;
static get instance(): ForesightManager;
get registeredElements(): ReadonlyMap<ForesightElement, ForesightElementData>;
register({ element, callback, hitSlop, name, }: ForesightRegisterOptions): ForesightRegisterResult;
private unregister;
private updateNumericSettings;
private updateBooleanSetting;
alterGlobalSettings(props?: Partial<UpdateForsightManagerSettings>): void;
private forceUpdateAllElementBounds;
private updatePointerState;
/**
* Processes elements that unregister after a single callback.
*
* This is a "fire-and-forget" handler. Its only goal is to trigger the
* callback once. It does so if the mouse trajectory is predicted to hit the
* element (if prediction is on) OR if the mouse physically hovers over it.
* It does not track state, as the element is immediately unregistered.
*
* @param elementData - The data object for the foresight element.
* @param element - The HTML element being interacted with.
*/
private handleCallbackInteraction;
private handleMouseMove;
/**
* Detects when registered elements are removed from the DOM and automatically unregisters them to prevent stale references.
*
* @param mutationsList - Array of MutationRecord objects describing the DOM changes
*
*/
private handleDomMutations;
private handleKeyDown;
private handleFocusIn;
private updateHitCounters;
private callCallback;
/**
* 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.
* We need an observer for that
*/
private forceUpdateElementBounds;
private updateElementBounds;
private handleScrollPrefetch;
private handlePositionChange;
private initializeGlobalListeners;
private removeGlobalListeners;
}
export { ForesightManager };
export type { CallbackFiredEvent, ElementDataUpdatedEvent, ElementRegisteredEvent, ElementUnregisteredEvent, CallbackHits as ForesightCallbackHits, ForesightElement, ForesightElementData, ForesightEvent, ForesightManagerSettings, Rect as ForesightRect, ForesightRegisterOptions, ForesightRegisterOptionsWithoutElement, ForesightRegisterResult, HitSlop, ManagerSettingsChangedEvent, MouseTrajectoryUpdateEvent, ScrollTrajectoryUpdateEvent, UpdateForsightManagerSettings };