js.foresight
Version:
Predicts where users will click based on mouse movement, keyboard navigation, and scroll behavior. Includes touch device support. Triggers callbacks before interactions happen to enable prefetching and faster UI responses. Works with any framework.
510 lines (506 loc) • 19.9 kB
TypeScript
declare class CircularBuffer<T> {
private buffer;
private head;
private count;
private capacity;
constructor(capacity: number);
add(item: T): void;
getFirst(): T | undefined;
getLast(): T | undefined;
getFirstLast(): [T | undefined, T | undefined];
resize(newCapacity: number): void;
private getAllItems;
clear(): void;
get length(): number;
get size(): number;
get isFull(): boolean;
get isEmpty(): boolean;
}
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: CircularBuffer<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: Readonly<Rect>;
/** The original bounding rectangle of the element, as returned by `getBoundingClientRect()`. */
originalRect: DOMRectReadOnly;
/** The hit slop values applied to this element. */
hitSlop: Exclude<HitSlop, number>;
};
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
* @deprecated As of version 3.3, ForesightJS handles touch devices internally with dedicated touch strategies
*/
isTouchDevice: boolean;
/** Whether the user has connection limitations (network slower than minimum connection type (default: 3g) 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
* @deprecated no longer need to call this manually, you can call Foresightmanager.instance.unregister if needed
*/
unregister: () => void;
};
/**
* Represents the data associated with a registered foresight element.
*/
type ForesightElementData = Required<Pick<ForesightRegisterOptions, "callback" | "name" | "meta">> & {
/** Unique identifier assigned during registration */
id: string;
/** The boundary information for the element. */
elementBounds: ElementBounds;
/**
* Is the element intersecting with the viewport, usefull to track which element we should observe or not
* Can be @undefined in the split second the element is registering
*/
isIntersectingWithViewport: boolean;
/**
* The element you registered
*/
element: ForesightElement;
/**
* For debugging, check if you are registering the same element multiple times.
*/
registerCount: number;
/**
* Callbackinfo for debugging purposes
*/
callbackInfo: ElementCallbackInfo;
};
type ElementCallbackInfo = {
/**
* Number of times the callback has been fired for this element
*/
callbackFiredCount: number;
/**
* Timestamp when the callback was last fired
*/
lastCallbackInvokedAt: number | undefined;
/**
* Timestamp when the last callback was finished
*/
lastCallbackCompletedAt: number | undefined;
/**
* Time in milliseconds it took for the last callback to go from invoked to complete.
*/
lastCallbackRuntime: number | undefined;
/**
* Status of the last ran callback
*/
lastCallbackStatus: callbackStatus;
/**
* Last callback error message
*/
lastCallbackErrorMessage: string | undefined | null;
/**
* Time in milliseconds after which the callback can be fired again
*/
reactivateAfter: number;
/**
* Whether the callback is currently active (within stale time period)
*/
isCallbackActive: boolean;
/**
* If the element is currently running its callback
*/
isRunningCallback: boolean;
/**
* Timeout ID for the scheduled reactivation, if any
*/
reactivateTimeoutId?: ReturnType<typeof setTimeout>;
};
type callbackStatus = "error" | "success" | undefined;
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;
touch: number;
viewport: number;
};
type CallbackHitType = {
kind: "mouse";
subType: keyof MouseCallbackCounts;
} | {
kind: "tab";
subType: keyof TabCallbackCounts;
} | {
kind: "scroll";
subType: keyof ScrollCallbackCounts;
} | {
kind: "touch";
subType?: string;
} | {
kind: "viewport";
subType?: string;
};
/**
* Snapshot of the current ForesightManager state
*/
type ForesightManagerData = {
registeredElements: ReadonlyMap<ForesightElement, ForesightElementData>;
globalSettings: Readonly<ForesightManagerSettings>;
globalCallbackHits: Readonly<CallbackHits>;
eventListeners: ReadonlyMap<keyof ForesightEventMap, ForesightEventListener[]>;
currentDeviceStrategy: CurrentDeviceStrategy;
activeElementCount: number;
};
type TouchDeviceStrategy = "none" | "viewport" | "onTouchStart";
type MinimumConnectionType = "slow-2g" | "2g" | "3g" | "4g";
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;
/**
*
* Logs basic information about the ForesightManager and its handlers that doesn't have a dedicated event.
*
* Mostly used by the maintainers of ForesightJS to debug the manager. But might be useful for implementers aswell.
*
* This is not the same as logging events, this can be done with the actual developer tools.
* @link https://foresightjs.com/docs/debugging/devtools
*/
enableManagerLogging: 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;
/**
* The prefetch strategy used for touch devices.
* - `viewport`: Prefetching is done based on the viewport, meaning elements in the viewport are preloaded.
* - `onTouchStart`: Prefetching is done when the user touches the element
* @default onTouchStart
*/
touchDeviceStrategy: TouchDeviceStrategy;
/**
* Network effective connection types that should be considered as limited connections.
* When the user's network matches any of these types, ForesightJS will disable prefetching
* to avoid consuming data on slow or expensive connections.
*
* @link https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType
* @default 3g
*/
minimumConnectionType: MinimumConnectionType;
};
type CurrentDeviceStrategy = "mouse" | "touch" | "pen";
/**
* 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;
/**
* If set by user, stores additional information about the registered element
*/
meta?: Record<string, unknown>;
/**
* Time in milliseconds after which the callback can be fired again and we reactivate the element.
* Set to Infinity to prevent callback from firing again after first execution.
* @default Infinity
*/
reactivateAfter?: number;
};
/**
* 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;
elementReactivated: ElementReactivatedEvent;
elementUnregistered: ElementUnregisteredEvent;
elementDataUpdated: ElementDataUpdatedEvent;
callbackInvoked: CallbackInvokedEvent;
callbackCompleted: CallbackCompletedEvent;
mouseTrajectoryUpdate: MouseTrajectoryUpdateEvent;
scrollTrajectoryUpdate: ScrollTrajectoryUpdateEvent;
managerSettingsChanged: ManagerSettingsChangedEvent;
deviceStrategyChanged: DeviceStrategyChangedEvent;
}
type ForesightEvent = "elementRegistered" | "elementReactivated" | "elementUnregistered" | "elementDataUpdated" | "callbackInvoked" | "callbackCompleted" | "mouseTrajectoryUpdate" | "scrollTrajectoryUpdate" | "managerSettingsChanged" | "deviceStrategyChanged";
interface DeviceStrategyChangedEvent extends ForesightBaseEvent {
type: "deviceStrategyChanged";
newStrategy: CurrentDeviceStrategy;
oldStrategy: CurrentDeviceStrategy;
}
interface ElementRegisteredEvent extends ForesightBaseEvent {
type: "elementRegistered";
elementData: ForesightElementData;
}
interface ElementReactivatedEvent extends ForesightBaseEvent {
type: "elementReactivated";
elementData: ForesightElementData;
}
interface ElementUnregisteredEvent extends ForesightBaseEvent {
type: "elementUnregistered";
elementData: ForesightElementData;
unregisterReason: ElementUnregisteredReason;
wasLastRegisteredElement: boolean;
}
/**
* 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.
* - `devtools`: When clicking the trash icon in the devtools element tab
* - any other string
*/
type ElementUnregisteredReason = "disconnected" | "apiCall" | "devtools" | (string & {});
interface ElementDataUpdatedEvent extends Omit<ForesightBaseEvent, "timestamp"> {
type: "elementDataUpdated";
elementData: ForesightElementData;
updatedProps: UpdatedDataPropertyNames[];
}
type UpdatedDataPropertyNames = "bounds" | "visibility";
interface CallbackInvokedEvent extends ForesightBaseEvent {
type: "callbackInvoked";
elementData: ForesightElementData;
hitType: CallbackHitType;
}
interface CallbackCompletedEventBase extends ForesightBaseEvent {
type: "callbackCompleted";
elementData: ForesightElementData;
hitType: CallbackHitType;
elapsed: number;
wasLastActiveElement: boolean;
}
type CallbackCompletedEvent = CallbackCompletedEventBase & {
status: callbackStatus;
errorMessage: string | undefined | null;
};
interface MouseTrajectoryUpdateEvent extends Omit<ForesightBaseEvent, "timestamp"> {
type: "mouseTrajectoryUpdate";
trajectoryPositions: TrajectoryPositions;
predictionEnabled: boolean;
}
interface ScrollTrajectoryUpdateEvent extends Omit<ForesightBaseEvent, "timestamp"> {
type: "scrollTrajectoryUpdate";
currentPoint: Point;
predictedPoint: Point;
scrollDirection: ScrollDirection;
}
interface ManagerSettingsChangedEvent extends ForesightBaseEvent {
type: "managerSettingsChanged";
managerData: ForesightManagerData;
updatedSettings: UpdatedManagerSetting[];
}
type UpdatedManagerSetting = {
[K in keyof ForesightManagerSettings]: {
setting: K;
newValue: ForesightManagerSettings[K];
oldValue: ForesightManagerSettings[K];
};
}[keyof ForesightManagerSettings];
type ForesightEventListener<K extends ForesightEvent = ForesightEvent> = (event: ForesightEventMap[K]) => void;
interface ForesightBaseEvent {
type: ForesightEvent;
timestamp: number;
}
/**
* 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 idCounter;
private activeElementCount;
private desktopHandler;
private touchDeviceHandler;
private handler;
private isSetup;
private _globalCallbackHits;
private _globalSettings;
private pendingPointerEvent;
private rafId;
private domObserver;
private eventListeners;
private currentDeviceStrategy;
private constructor();
private generateId;
static initialize(props?: Partial<UpdateForsightManagerSettings>): ForesightManager;
addEventListener<K extends ForesightEvent>(eventType: K, listener: ForesightEventListener<K>, options?: {
signal?: AbortSignal;
}): (() => void) | undefined;
removeEventListener<K extends ForesightEvent>(eventType: K, listener: ForesightEventListener<K>): 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, meta, reactivateAfter, }: ForesightRegisterOptions): ForesightRegisterResult;
unregister(element: ForesightElement, unregisterReason?: ElementUnregisteredReason): void;
private updateHitCounters;
reactivate(element: ForesightElement): void;
private clearReactivateTimeout;
private makeElementUnactive;
private callCallback;
private setDeviceStrategy;
private handlePointerMove;
private initializeGlobalListeners;
private removeGlobalListeners;
/**
* 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 forceUpdateAllElementBounds;
/**
* 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 initializeManagerSettings;
private updateNumericSettings;
private updateBooleanSetting;
alterGlobalSettings(props?: Partial<UpdateForsightManagerSettings>): void;
private devLog;
}
export { type CallbackCompletedEvent, type CallbackHitType, type CallbackHits, type CallbackInvokedEvent, type DeviceStrategyChangedEvent, type ElementCallbackInfo, type ElementDataUpdatedEvent, type ElementReactivatedEvent, 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 MinimumConnectionType, type MouseTrajectoryUpdateEvent, type ScrollTrajectoryUpdateEvent, type TouchDeviceStrategy, type UpdateForsightManagerSettings, type UpdatedManagerSetting };