UNPKG

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
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 };