js.foresight 3.3.0 → 3.3.2

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
@@ -44,12 +44,12 @@ ForesightManager.initialize({
44
44
  // Register an element to be tracked
45
45
  const myButton = document.getElementById("my-button")
46
46
 
47
- const { isTouchDevice } = ForesightManager.instance.register({
47
+ ForesightManager.instance.register({
48
48
  element: myButton,
49
49
  callback: () => {
50
50
  // This is where your prefetching logic goes
51
51
  },
52
- hitSlop: 20, // Optional: "hit slop" in pixels. Overwrites defaultHitSlop
52
+ hitSlop: 20, // Optional: "hit slop" in pixels.
53
53
  // other optional props (see configuration)
54
54
  })
55
55
  ```
package/dist/index.d.ts CHANGED
@@ -69,7 +69,7 @@ type ForesightRegisterResult = {
69
69
  * @deprecated As of version 3.3, ForesightJS handles touch devices internally with dedicated touch strategies
70
70
  */
71
71
  isTouchDevice: boolean;
72
- /** Whether the user has connection limitations (slow network (2g) or data saver enabled) that should prevent prefetching */
72
+ /** Whether the user has connection limitations (network slower than minimum connection type (default: 3g) or data saver enabled) that should prevent prefetching */
73
73
  isLimitedConnection: boolean;
74
74
  /** Whether ForesightJS will actively track this element. False if touch device or limited connection, true otherwise */
75
75
  isRegistered: boolean;
@@ -193,6 +193,7 @@ type ForesightManagerData = {
193
193
  activeElementCount: number;
194
194
  };
195
195
  type TouchDeviceStrategy = "none" | "viewport" | "onTouchStart";
196
+ type MinimumConnectionType = "slow-2g" | "2g" | "3g" | "4g";
196
197
  type BaseForesightManagerSettings = {
197
198
  /**
198
199
  * Number of mouse positions to keep in history for trajectory calculation.
@@ -213,6 +214,16 @@ type BaseForesightManagerSettings = {
213
214
  * @link https://github.com/spaansba/ForesightJS-DevTools
214
215
  */
215
216
  debug: boolean;
217
+ /**
218
+ *
219
+ * Logs basic information about the ForesightManager and its handlers that doesn't have a dedicated event.
220
+ *
221
+ * Mostly used by the maintainers of ForesightJS to debug the manager. But might be useful for implementers aswell.
222
+ *
223
+ * This is not the same as logging events, this can be done with the actual developer tools.
224
+ * @link https://foresightjs.com/docs/debugging/devtools
225
+ */
226
+ enableManagerLogging: boolean;
216
227
  /**
217
228
  * How far ahead (in milliseconds) to predict the mouse trajectory.
218
229
  * A larger value means the prediction extends further into the future. (meaning it will trigger callbacks sooner)
@@ -261,12 +272,20 @@ type BaseForesightManagerSettings = {
261
272
  tabOffset: number;
262
273
  /**
263
274
  * The prefetch strategy used for touch devices.
264
- * - `none`: No prefetching is done on touch devices.
265
275
  * - `viewport`: Prefetching is done based on the viewport, meaning elements in the viewport are preloaded.
266
276
  * - `onTouchStart`: Prefetching is done when the user touches the element
267
277
  * @default onTouchStart
268
278
  */
269
279
  touchDeviceStrategy: TouchDeviceStrategy;
280
+ /**
281
+ * Network effective connection types that should be considered as limited connections.
282
+ * When the user's network matches any of these types, ForesightJS will disable prefetching
283
+ * to avoid consuming data on slow or expensive connections.
284
+ *
285
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType
286
+ * @default 3g
287
+ */
288
+ minimumConnectionType: MinimumConnectionType;
270
289
  };
271
290
  type CurrentDeviceStrategy = "mouse" | "touch" | "pen";
272
291
  /**
@@ -484,6 +503,7 @@ declare class ForesightManager {
484
503
  private updateNumericSettings;
485
504
  private updateBooleanSetting;
486
505
  alterGlobalSettings(props?: Partial<UpdateForsightManagerSettings>): void;
506
+ private devLog;
487
507
  }
488
508
 
489
- 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 MouseTrajectoryUpdateEvent, type ScrollTrajectoryUpdateEvent, type TouchDeviceStrategy, type UpdateForsightManagerSettings, type UpdatedManagerSetting };
509
+ 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 };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- function p(n,e,t,i){return n<e?console.warn(`ForesightJS: "${i}" value ${n} is below minimum bound ${e}, clamping to ${e}`):n>t&&console.warn(`ForesightJS: "${i}" value ${n} is above maximum bound ${t}, clamping to ${t}`),Math.min(Math.max(n,e),t)}function z(n){if(typeof window>"u"||typeof document>"u")return!1;let e=window.innerWidth||document.documentElement.clientWidth,t=window.innerHeight||document.documentElement.clientHeight;return n.top<t&&n.bottom>0&&n.left<e&&n.right>0}function T(n){if(typeof n=="number"){let e=p(n,0,2e3,"hitslop");return{top:e,left:e,right:e,bottom:e}}return{top:p(n.top,0,2e3,"hitslop - top"),left:p(n.left,0,2e3,"hitslop - left"),right:p(n.right,0,2e3,"hitslop - right"),bottom:p(n.bottom,0,2e3,"hitslop - bottom")}}function P(n,e){return{left:n.left-e.left,right:n.right+e.right,top:n.top-e.top,bottom:n.bottom+e.bottom}}function w(n,e){return!n||!e?n===e:n.left===e.left&&n.right===e.right&&n.top===e.top&&n.bottom===e.bottom}function C(n,e){return n.x>=e.left&&n.x<=e.right&&n.y>=e.top&&n.y<=e.bottom}function K(){let n=A(),e=te();return{isTouchDevice:n,isLimitedConnection:e,shouldRegister:!e}}function A(){return typeof window>"u"||typeof navigator>"u"?!1:window.matchMedia("(pointer: coarse)").matches&&navigator.maxTouchPoints>0}function te(){let n=navigator.connection;return n?/2g/.test(n.effectiveType)||n.saveData:!1}function N(n,e){return n!==void 0&&e!==n}var u=class{constructor(e){this._isConnected=!1;this.elements=e.elements,this.callCallback=e.callCallback,this.emit=e.emit,this.settings=e.settings}get isConnected(){return this._isConnected}disconnect(){this.isConnected&&(this.devLog(`Disconnecting ${this.moduleName}...`),this.abortController?.abort(`${this.moduleName} module disconnected`),this.onDisconnect(),this._isConnected=!1)}connect(){this.devLog(`Connecting ${this.moduleName}...`),this.onConnect(),this._isConnected=!0}devLog(e){process.env.NODE_ENV==="development"&&console.log(`\u{1F6E0}\uFE0F ${this.moduleName}: ${e}`)}createAbortController(){this.abortController&&!this.abortController.signal.aborted||(this.abortController=new AbortController,this.devLog(`Created new AbortController for ${this.moduleName}`))}};function I(n,e,t){let i=0,o=1,r=e.x-n.x,l=e.y-n.y,a=(s,d)=>{if(s===0){if(d<0)return!1}else{let c=d/s;if(s<0){if(c>o)return!1;c>i&&(i=c)}else{if(c<i)return!1;c<o&&(o=c)}}return!0};return!a(-r,n.x-t.left)||!a(r,t.right-n.x)||!a(-l,n.y-t.top)||!a(l,t.bottom-n.y)?!1:i<=o}function Y(n,e,t){let i=performance.now(),o={point:n,time:i},{x:r,y:l}=n;if(e.add(o),e.length<2)return{x:r,y:l};let[a,s]=e.getFirstLast();if(!a||!s)return{x:r,y:l};let d=(s.time-a.time)*.001;if(d===0)return{x:r,y:l};let c=s.point.x-a.point.x,h=s.point.y-a.point.y,y=c/d,g=h/d,x=t*.001,L=r+y*x,V=l+g*x;return{x:L,y:V}}var F=class extends u{constructor(t){super(t.dependencies);this.moduleName="MousePredictor";this.trajectoryPositions=t.trajectoryPositions}updatePointerState(t){let i={x:t.clientX,y:t.clientY};this.trajectoryPositions.currentPoint=i,this.settings.enableMousePrediction?this.trajectoryPositions.predictedPoint=Y(i,this.trajectoryPositions.positions,this.settings.trajectoryPredictionTime):this.trajectoryPositions.predictedPoint=i}processMouseMovement(t){this.updatePointerState(t);let i=this.settings.enableMousePrediction,o=this.trajectoryPositions.currentPoint;for(let r of this.elements.values()){if(!r.isIntersectingWithViewport||!r.callbackInfo.isCallbackActive||r.callbackInfo.isRunningCallback)continue;let l=r.elementBounds.expandedRect;if(i)I(o,this.trajectoryPositions.predictedPoint,l)&&this.callCallback(r,{kind:"mouse",subType:"trajectory"});else if(C(o,l)){this.callCallback(r,{kind:"mouse",subType:"hover"});return}}this.emit({type:"mouseTrajectoryUpdate",predictionEnabled:i,trajectoryPositions:this.trajectoryPositions})}onDisconnect(){}onConnect(){}};function $(n,e){let i=e.top-n.top,o=e.left-n.left;return i<-1?"down":i>1?"up":o<-1?"right":o>1?"left":"none"}function G(n,e,t){let{x:i,y:o}=n,r={x:i,y:o};switch(e){case"down":r.y+=t;break;case"up":r.y-=t;break;case"left":r.x-=t;break;case"right":r.x+=t;break;case"none":break;default:}return r}var M=class extends u{constructor(t){super(t.dependencies);this.moduleName="ScrollPredictor";this.predictedScrollPoint=null;this.scrollDirection=null;this.onDisconnect=()=>this.resetScrollProps();this.trajectoryPositions=t.trajectoryPositions}resetScrollProps(){this.scrollDirection=null,this.predictedScrollPoint=null}handleScrollPrefetch(t,i){!t.isIntersectingWithViewport||t.callbackInfo.isRunningCallback||!t.callbackInfo.isCallbackActive||(this.scrollDirection=this.scrollDirection??$(t.elementBounds.originalRect,i),this.scrollDirection!=="none"&&(this.predictedScrollPoint=this.predictedScrollPoint??G(this.trajectoryPositions.currentPoint,this.scrollDirection,this.settings.scrollMargin),I(this.trajectoryPositions.currentPoint,this.predictedScrollPoint,t.elementBounds.expandedRect)&&this.callCallback(t,{kind:"scroll",subType:this.scrollDirection}),this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.predictedScrollPoint,scrollDirection:this.scrollDirection})))}onConnect(){}};import{tabbable as ie}from"tabbable";function X(n,e,t,i){if(e!==null&&e>-1){let o=n?e-1:e+1;if(o>=0&&o<t.length&&t[o]===i)return o}return t.findIndex(o=>o===i)}var D=class extends u{constructor(t){super(t);this.moduleName="TabPredictor";this.lastKeyDown=null;this.tabbableElementsCache=[];this.lastFocusedIndex=null;this.handleKeyDown=t=>{t.key==="Tab"&&(this.lastKeyDown=t)};this.handleFocusIn=t=>{if(!this.lastKeyDown)return;let i=t.target;if(!(i instanceof HTMLElement))return;(!this.tabbableElementsCache.length||this.lastFocusedIndex===-1)&&(this.tabbableElementsCache=ie(document.documentElement));let o=this.lastKeyDown.shiftKey,r=X(o,this.lastFocusedIndex,this.tabbableElementsCache,i);this.lastFocusedIndex=r,this.lastKeyDown=null;let l=[],a=this.settings.tabOffset,s=this.elements;for(let d=0;d<=a;d++){let c=o?r-d:r+d,h=this.tabbableElementsCache[c];h&&h instanceof Element&&s.has(h)&&l.push(h)}for(let d of l){let c=s.get(d);c&&!c.callbackInfo.isRunningCallback&&c.callbackInfo.isCallbackActive&&this.callCallback(c,{kind:"tab",subType:o?"reverse":"forwards"})}}}invalidateCache(){this.tabbableElementsCache=[],this.lastFocusedIndex=null}onConnect(){typeof document>"u"||(this.createAbortController(),document.addEventListener("keydown",this.handleKeyDown,{signal:this.abortController?.signal,passive:!0}),document.addEventListener("focusin",this.handleFocusIn,{signal:this.abortController?.signal,passive:!0}))}onDisconnect(){this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.lastKeyDown=null}};import{PositionObserver as ne}from"position-observer";var R=class{constructor(e){this.head=0;this.count=0;if(e<=0)throw new Error("CircularBuffer capacity must be greater than 0");this.capacity=e,this.buffer=new Array(e)}add(e){this.buffer[this.head]=e,this.head=(this.head+1)%this.capacity,this.count<this.capacity&&this.count++}getFirst(){if(this.count!==0)return this.count<this.capacity?this.buffer[0]:this.buffer[this.head]}getLast(){if(this.count!==0){if(this.count<this.capacity)return this.buffer[this.count-1];{let e=(this.head-1+this.capacity)%this.capacity;return this.buffer[e]}}}getFirstLast(){if(this.count===0)return[void 0,void 0];if(this.count===1){let i=this.count<this.capacity?this.buffer[0]:this.buffer[this.head];return[i,i]}let e=this.getFirst(),t=this.getLast();return[e,t]}resize(e){if(e<=0)throw new Error("CircularBuffer capacity must be greater than 0");if(e===this.capacity)return;let t=this.getAllItems();if(this.capacity=e,this.buffer=new Array(e),this.head=0,this.count=0,t.length>e){let i=t.slice(-e);for(let o of i)this.add(o)}else for(let i of t)this.add(i)}getAllItems(){if(this.count===0)return[];let e=new Array(this.count);if(this.count<this.capacity)for(let t=0;t<this.count;t++)e[t]=this.buffer[t];else{let t=this.head;for(let i=0;i<this.capacity;i++){let o=(t+i)%this.capacity;e[i]=this.buffer[o]}}return e}clear(){this.head=0,this.count=0}get length(){return this.count}get size(){return this.capacity}get isFull(){return this.count===this.capacity}get isEmpty(){return this.count===0}};var f=class extends u{constructor(t){super(t);this.moduleName="DesktopHandler";this.positionObserver=null;this.trajectoryPositions={positions:new R(8),currentPoint:{x:0,y:0},predictedPoint:{x:0,y:0}};this.handlePositionChange=t=>{let i=this.settings.enableScrollPrediction;for(let o of t){let r=this.elements.get(o.target);r&&(i?this.scrollPredictor?.handleScrollPrefetch(r,o.boundingClientRect):this.checkForMouseHover(r),this.handlePositionChangeDataUpdates(r,o))}i&&this.scrollPredictor?.resetScrollProps()};this.checkForMouseHover=t=>{C(this.trajectoryPositions.currentPoint,t.elementBounds.expandedRect)&&this.callCallback(t,{kind:"mouse",subType:"hover"})};this.handlePositionChangeDataUpdates=(t,i)=>{let o=[],r=i.isIntersecting;t.isIntersectingWithViewport!==r&&(o.push("visibility"),t.isIntersectingWithViewport=r),r&&(o.push("bounds"),t.elementBounds={hitSlop:t.elementBounds.hitSlop,originalRect:i.boundingClientRect,expandedRect:P(i.boundingClientRect,t.elementBounds.hitSlop)}),o.length&&this.emit({type:"elementDataUpdated",elementData:t,updatedProps:o})};this.processMouseMovement=t=>this.mousePredictor.processMouseMovement(t);this.invalidateTabCache=()=>this.tabPredictor?.invalidateCache();this.observeElement=t=>this.positionObserver?.observe(t);this.unobserveElement=t=>this.positionObserver?.unobserve(t);this.connectTabPredictor=()=>this.tabPredictor.connect();this.connectScrollPredictor=()=>this.scrollPredictor.connect();this.connectMousePredictor=()=>this.mousePredictor.connect();this.disconnectTabPredictor=()=>this.tabPredictor.disconnect();this.disconnectScrollPredictor=()=>this.scrollPredictor.disconnect();this.disconnectMousePredictor=()=>this.mousePredictor.disconnect();this.tabPredictor=new D(t),this.scrollPredictor=new M({dependencies:t,trajectoryPositions:this.trajectoryPositions}),this.mousePredictor=new F({dependencies:t,trajectoryPositions:this.trajectoryPositions})}onConnect(){this.settings.enableTabPrediction&&this.connectTabPredictor(),this.settings.enableScrollPrediction&&this.connectScrollPredictor(),this.connectMousePredictor(),this.positionObserver=new ne(this.handlePositionChange);for(let t of this.elements.keys())this.positionObserver.observe(t)}onDisconnect(){this.disconnectMousePredictor(),this.disconnectTabPredictor(),this.disconnectScrollPredictor(),this.positionObserver?.disconnect(),this.positionObserver=null}};var _=class extends u{constructor(t){super(t);this.moduleName="ViewportPredictor";this.intersectionObserver=null;this.onConnect=()=>this.intersectionObserver=new IntersectionObserver(this.handleViewportEnter);this.observeElement=t=>this.intersectionObserver?.observe(t);this.unobserveElement=t=>this.intersectionObserver?.unobserve(t);this.handleViewportEnter=t=>{for(let i of t){if(!i.isIntersecting)continue;let o=this.elements.get(i.target);o&&(this.callCallback(o,{kind:"viewport"}),this.unobserveElement(i.target))}}}onDisconnect(){this.intersectionObserver?.disconnect(),this.intersectionObserver=null}};var k=class extends u{constructor(t){super(t);this.moduleName="TouchStartPredictor";this.onConnect=()=>this.createAbortController();this.onDisconnect=()=>{};this.handleTouchStart=t=>{let i=t.target,o=this.elements.get(i);o&&(this.callCallback(o,{kind:"touch"}),this.unobserveElement(i))}}observeElement(t){t instanceof HTMLElement&&t.addEventListener("touchstart",this.handleTouchStart,{signal:this.abortController?.signal})}unobserveElement(t){t instanceof HTMLElement&&t.removeEventListener("touchstart",this.handleTouchStart)}};var v=class extends u{constructor(t){super(t);this.moduleName="TouchDeviceHandler";this.predictor=null;this.onDisconnect=()=>this.predictor?.disconnect();this.onConnect=()=>this.setTouchPredictor();this.observeElement=t=>this.predictor?.observeElement(t);this.unobserveElement=t=>this.predictor?.unobserveElement(t);this.viewportPredictor=new _(t),this.touchStartPredictor=new k(t),this.predictor=this.viewportPredictor}setTouchPredictor(){switch(this.predictor?.disconnect(),this.settings.touchDeviceStrategy){case"viewport":this.predictor=this.viewportPredictor;break;case"onTouchStart":this.predictor=this.touchStartPredictor;break;case"none":this.predictor=null;return;default:this.settings.touchDeviceStrategy}this.predictor?.connect();for(let t of this.elements.keys())this.predictor?.observeElement(t)}};var B=class n{constructor(e){this.elements=new Map;this.idCounter=0;this.activeElementCount=0;this.isSetup=!1;this._globalCallbackHits={mouse:{hover:0,trajectory:0},tab:{forwards:0,reverse:0},scroll:{down:0,left:0,right:0,up:0},touch:0,viewport:0,total:0};this._globalSettings={debug:!1,enableMousePrediction:!0,enableScrollPrediction:!0,positionHistorySize:8,trajectoryPredictionTime:120,scrollMargin:150,defaultHitSlop:{top:0,left:0,right:0,bottom:0},enableTabPrediction:!0,tabOffset:2,touchDeviceStrategy:"onTouchStart"};this.pendingPointerEvent=null;this.rafId=null;this.domObserver=null;this.eventListeners=new Map;this.currentDeviceStrategy=A()?"touch":"mouse";this.handlePointerMove=e=>{this.pendingPointerEvent=e,e.pointerType!=this.currentDeviceStrategy&&(this.emit({type:"deviceStrategyChanged",timestamp:Date.now(),newStrategy:e.pointerType,oldStrategy:this.currentDeviceStrategy}),this.setDeviceStrategy(this.currentDeviceStrategy=e.pointerType)),!this.rafId&&(this.rafId=requestAnimationFrame(()=>{if(this.handler instanceof v){this.rafId=null;return}this.pendingPointerEvent&&this.handler.processMouseMovement(this.pendingPointerEvent),this.rafId=null}))};this.handleDomMutations=e=>{if(!e.length)return;this.desktopHandler?.invalidateTabCache();let t=!1;for(let i=0;i<e.length;i++){let o=e[i];if(o.type==="childList"&&o.removedNodes.length>0){t=!0;break}}if(t)for(let i of this.elements.keys())i.isConnected||this.unregister(i,"disconnected")};e!==void 0&&this.initializeManagerSettings(e);let t={elements:this.elements,callCallback:this.callCallback.bind(this),emit:this.emit.bind(this),settings:this._globalSettings};this.desktopHandler=new f(t),this.touchDeviceHandler=new v(t),this.handler=this.currentDeviceStrategy==="mouse"?this.desktopHandler:this.touchDeviceHandler,this.initializeGlobalListeners()}generateId(){return`foresight-${++this.idCounter}`}static initialize(e){return this.isInitiated||(n.manager=new n(e)),n.manager}addEventListener(e,t,i){if(i?.signal?.aborted)return()=>{};let o=this.eventListeners.get(e)??[];o.push(t),this.eventListeners.set(e,o),i?.signal?.addEventListener("abort",()=>this.removeEventListener(e,t))}removeEventListener(e,t){let i=this.eventListeners.get(e);if(!i)return;let o=i.indexOf(t);o>-1&&i.splice(o,1)}emit(e){let t=this.eventListeners.get(e.type)?.slice();if(t)for(let i=0;i<t.length;i++)try{t[i](e)}catch(o){console.error(`Error in ForesightManager event listener ${i} for ${e.type}:`,o)}}get getManagerData(){return{registeredElements:this.elements,globalSettings:this._globalSettings,globalCallbackHits:this._globalCallbackHits,eventListeners:this.eventListeners,currentDeviceStrategy:this.currentDeviceStrategy,activeElementCount:this.activeElementCount}}static get isInitiated(){return!!n.manager}static get instance(){return this.initialize()}get registeredElements(){return this.elements}register({element:e,callback:t,hitSlop:i,name:o,meta:r,reactivateAfter:l}){let{isTouchDevice:a,isLimitedConnection:s,shouldRegister:d}=K();if(!d)return{isLimitedConnection:s,isTouchDevice:a,isRegistered:!1,unregister:()=>{}};let c=this.elements.get(e);if(c)return c.registerCount++,{isLimitedConnection:s,isTouchDevice:a,isRegistered:!1,unregister:()=>{}};this.isSetup||this.initializeGlobalListeners();let h=e.getBoundingClientRect(),y=i?T(i):this._globalSettings.defaultHitSlop,g={id:this.generateId(),element:e,callback:t,elementBounds:{originalRect:h,expandedRect:P(h,y),hitSlop:y},name:o||e.id||"unnamed",isIntersectingWithViewport:z(h),registerCount:1,meta:r??{},callbackInfo:{callbackFiredCount:0,lastCallbackInvokedAt:void 0,lastCallbackCompletedAt:void 0,lastCallbackRuntime:void 0,lastCallbackStatus:void 0,lastCallbackErrorMessage:void 0,reactivateAfter:l??1/0,isCallbackActive:!0,isRunningCallback:!1,reactivateTimeoutId:void 0}};return this.elements.set(e,g),this.activeElementCount++,this.handler.observeElement(e),this.emit({type:"elementRegistered",timestamp:Date.now(),elementData:g}),{isTouchDevice:a,isLimitedConnection:s,isRegistered:!0,unregister:()=>{this.unregister(e)}}}unregister(e,t){let i=this.elements.get(e);if(!i)return;this.clearReactivateTimeout(i),this.handler.unobserveElement(e),this.elements.delete(e),i.callbackInfo.isCallbackActive&&this.activeElementCount--;let o=this.elements.size===0&&this.isSetup;o&&this.removeGlobalListeners(),i&&this.emit({type:"elementUnregistered",elementData:i,timestamp:Date.now(),unregisterReason:t??"by user",wasLastRegisteredElement:o})}updateHitCounters(e){switch(e.kind){case"mouse":this._globalCallbackHits.mouse[e.subType]++;break;case"tab":this._globalCallbackHits.tab[e.subType]++;break;case"scroll":this._globalCallbackHits.scroll[e.subType]++;break;case"touch":this._globalCallbackHits.touch++;break;case"viewport":this._globalCallbackHits.viewport++;break;default:}this._globalCallbackHits.total++}reactivate(e){let t=this.elements.get(e);t&&(this.isSetup||this.initializeGlobalListeners(),this.clearReactivateTimeout(t),t.callbackInfo.isRunningCallback||(t.callbackInfo.isCallbackActive=!0,this.activeElementCount++,this.handler.observeElement(e),this.emit({type:"elementReactivated",elementData:t,timestamp:Date.now()})))}clearReactivateTimeout(e){clearTimeout(e.callbackInfo.reactivateTimeoutId),e.callbackInfo.reactivateTimeoutId=void 0}makeElementUnactive(e){e.callbackInfo.callbackFiredCount++,e.callbackInfo.lastCallbackInvokedAt=Date.now(),e.callbackInfo.isRunningCallback=!0,this.clearReactivateTimeout(e)}callCallback(e,t){if(e.callbackInfo.isRunningCallback||!e.callbackInfo.isCallbackActive)return;this.makeElementUnactive(e),(async()=>{this.updateHitCounters(t),this.emit({type:"callbackInvoked",timestamp:Date.now(),elementData:e,hitType:t});let o=performance.now(),r,l=null;try{await e.callback(),r="success"}catch(s){l=s instanceof Error?s.message:String(s),r="error",console.error(`Error in callback for element ${e.name}:`,s)}e.callbackInfo.lastCallbackCompletedAt=Date.now(),e.callbackInfo.isRunningCallback=!1,e.callbackInfo.isCallbackActive=!1,this.activeElementCount--,this.handler.unobserveElement(e.element),e.callbackInfo.reactivateAfter!==1/0&&(e.callbackInfo.reactivateTimeoutId=setTimeout(()=>{this.reactivate(e.element)},e.callbackInfo.reactivateAfter));let a=this.activeElementCount===0;a&&this.removeGlobalListeners(),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:e,hitType:t,elapsed:e.callbackInfo.lastCallbackRuntime=performance.now()-o,status:e.callbackInfo.lastCallbackStatus=r,errorMessage:e.callbackInfo.lastCallbackErrorMessage=l,wasLastActiveElement:a})})()}setDeviceStrategy(e){this.handler.disconnect(),this.handler=e==="mouse"?this.desktopHandler:this.touchDeviceHandler,this.handler.connect()}initializeGlobalListeners(){this.isSetup||typeof document>"u"||(this.setDeviceStrategy(this.currentDeviceStrategy),document.addEventListener("pointermove",this.handlePointerMove),this.domObserver=new MutationObserver(this.handleDomMutations),this.domObserver.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1}),this.isSetup=!0)}removeGlobalListeners(){typeof document>"u"||(this.isSetup=!1,this.domObserver?.disconnect(),this.domObserver=null,document.removeEventListener("pointermove",this.handlePointerMove),this.handler.disconnect(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.pendingPointerEvent=null)}forceUpdateAllElementBounds(){for(let[,e]of this.elements)e.isIntersectingWithViewport&&this.forceUpdateElementBounds(e)}forceUpdateElementBounds(e){let t=e.element.getBoundingClientRect(),i=P(t,e.elementBounds.hitSlop);if(!w(i,e.elementBounds.expandedRect)){let o={...e,elementBounds:{...e.elementBounds,originalRect:t,expandedRect:i}};this.elements.set(e.element,o),this.emit({type:"elementDataUpdated",elementData:o,updatedProps:["bounds"]})}}initializeManagerSettings(e){this.updateNumericSettings(e.trajectoryPredictionTime,"trajectoryPredictionTime",10,200),this.updateNumericSettings(e.positionHistorySize,"positionHistorySize",2,30),this.updateNumericSettings(e.scrollMargin,"scrollMargin",30,300),this.updateNumericSettings(e.tabOffset,"tabOffset",0,20),this.updateBooleanSetting(e.enableMousePrediction,"enableMousePrediction"),this.updateBooleanSetting(e.enableScrollPrediction,"enableScrollPrediction"),this.updateBooleanSetting(e.enableTabPrediction,"enableTabPrediction"),e.defaultHitSlop!==void 0&&(this._globalSettings.defaultHitSlop=T(e.defaultHitSlop)),e.touchDeviceStrategy!==void 0&&(this._globalSettings.touchDeviceStrategy=e.touchDeviceStrategy),e.debug!==void 0&&(this._globalSettings.debug=e.debug)}updateNumericSettings(e,t,i,o){return N(e,this._globalSettings[t])?(this._globalSettings[t]=p(e,i,o,t),!0):!1}updateBooleanSetting(e,t){return N(e,this._globalSettings[t])?(this._globalSettings[t]=e,!0):!1}alterGlobalSettings(e){let t=[],i=this._globalSettings.trajectoryPredictionTime;this.updateNumericSettings(e?.trajectoryPredictionTime,"trajectoryPredictionTime",10,200)&&t.push({setting:"trajectoryPredictionTime",oldValue:i,newValue:this._globalSettings.trajectoryPredictionTime});let r=this._globalSettings.positionHistorySize;this.updateNumericSettings(e?.positionHistorySize,"positionHistorySize",2,30)&&(t.push({setting:"positionHistorySize",oldValue:r,newValue:this._globalSettings.positionHistorySize}),this.desktopHandler.trajectoryPositions.positions.resize(this._globalSettings.positionHistorySize));let a=this._globalSettings.scrollMargin;if(this.updateNumericSettings(e?.scrollMargin,"scrollMargin",30,300)){let m=this._globalSettings.scrollMargin;t.push({setting:"scrollMargin",oldValue:a,newValue:m})}let d=this._globalSettings.tabOffset;this.updateNumericSettings(e?.tabOffset,"tabOffset",0,20)&&(this._globalSettings.tabOffset=p(e.tabOffset,0,20,"tabOffset"),t.push({setting:"tabOffset",oldValue:d,newValue:this._globalSettings.tabOffset}));let h=this._globalSettings.enableMousePrediction;this.updateBooleanSetting(e?.enableMousePrediction,"enableMousePrediction")&&t.push({setting:"enableMousePrediction",oldValue:h,newValue:this._globalSettings.enableMousePrediction});let g=this._globalSettings.enableScrollPrediction;this.updateBooleanSetting(e?.enableScrollPrediction,"enableScrollPrediction")&&(this.handler instanceof f&&(this._globalSettings.enableScrollPrediction?this.handler.connectScrollPredictor():this.handler.disconnectScrollPredictor()),t.push({setting:"enableScrollPrediction",oldValue:g,newValue:this._globalSettings.enableScrollPrediction}));let L=this._globalSettings.enableTabPrediction;if(this.updateBooleanSetting(e?.enableTabPrediction,"enableTabPrediction")&&(this.handler instanceof f&&(this._globalSettings.enableTabPrediction?this.handler.connectTabPredictor():this.handler.disconnectTabPredictor()),t.push({setting:"enableTabPrediction",oldValue:L,newValue:this._globalSettings.enableTabPrediction})),e?.defaultHitSlop!==void 0){let m=this._globalSettings.defaultHitSlop,b=T(e.defaultHitSlop);w(m,b)||(this._globalSettings.defaultHitSlop=b,t.push({setting:"defaultHitSlop",oldValue:m,newValue:b}),this.forceUpdateAllElementBounds())}if(e?.touchDeviceStrategy!==void 0){let m=this._globalSettings.touchDeviceStrategy,b=e.touchDeviceStrategy;this._globalSettings.touchDeviceStrategy=b,t.push({setting:"touchDeviceStrategy",oldValue:m,newValue:b}),this.handler instanceof v&&this.handler.setTouchPredictor()}t.length>0&&this.emit({type:"managerSettingsChanged",timestamp:Date.now(),managerData:this.getManagerData,updatedSettings:t})}};export{B as ForesightManager};
1
+ function g(n,e,t,i){return n<e?console.warn(`ForesightJS: "${i}" value ${n} is below minimum bound ${e}, clamping to ${e}`):n>t&&console.warn(`ForesightJS: "${i}" value ${n} is above maximum bound ${t}, clamping to ${t}`),Math.min(Math.max(n,e),t)}function z(n){if(typeof window>"u"||typeof document>"u")return!1;let e=window.innerWidth||document.documentElement.clientWidth,t=window.innerHeight||document.documentElement.clientHeight;return n.top<t&&n.bottom>0&&n.left<e&&n.right>0}function C(n){if(typeof n=="number"){let e=g(n,0,2e3,"hitslop");return{top:e,left:e,right:e,bottom:e}}return{top:g(n.top,0,2e3,"hitslop - top"),left:g(n.left,0,2e3,"hitslop - left"),right:g(n.right,0,2e3,"hitslop - right"),bottom:g(n.bottom,0,2e3,"hitslop - bottom")}}function P(n,e){return{left:n.left-e.left,right:n.right+e.right,top:n.top-e.top,bottom:n.bottom+e.bottom}}function A(n,e){return!n||!e?n===e:n.left===e.left&&n.right===e.right&&n.top===e.top&&n.bottom===e.bottom}function M(n,e){return n.x>=e.left&&n.x<=e.right&&n.y>=e.top&&n.y<=e.bottom}function K(){let n=N(),e=te();return{isTouchDevice:n,isLimitedConnection:e,shouldRegister:!e}}function N(){return typeof window>"u"||typeof navigator>"u"?!1:window.matchMedia("(pointer: coarse)").matches&&navigator.maxTouchPoints>0}function te(){let n=navigator.connection;if(!n)return!1;let e=T.instance.getManagerData.globalSettings.minimumConnectionType,t=["slow-2g","2g","3g","4g"],i=t.indexOf(n.effectiveType),o=t.indexOf(e);return i<o||n.saveData}function H(n,e){return n!==void 0&&e!==n}var u=class{constructor(e){this._isConnected=!1;this.elements=e.elements,this.callCallback=e.callCallback,this.emit=e.emit,this.settings=e.settings}get isConnected(){return this._isConnected}disconnect(){this.isConnected&&(this.devLog(`Disconnecting ${this.moduleName}...`),this.abortController?.abort(`${this.moduleName} module disconnected`),this.onDisconnect(),this._isConnected=!1)}connect(){this.devLog(`Connecting ${this.moduleName}...`),this.onConnect(),this._isConnected=!0}devLog(e){if(this.settings.enableManagerLogging){let t=this.moduleName.includes("Predictor")?"#ea580c":"#2563eb";console.log(`%c\u{1F6E0}\uFE0F ${this.moduleName}: ${e}`,`color: ${t}; font-weight: bold;`)}}createAbortController(){this.abortController&&!this.abortController.signal.aborted||(this.abortController=new AbortController,this.devLog(`Created new AbortController for ${this.moduleName}`))}};function I(n,e,t){let i=0,o=1,r=e.x-n.x,l=e.y-n.y,a=(s,d)=>{if(s===0){if(d<0)return!1}else{let c=d/s;if(s<0){if(c>o)return!1;c>i&&(i=c)}else{if(c<i)return!1;c<o&&(o=c)}}return!0};return!a(-r,n.x-t.left)||!a(r,t.right-n.x)||!a(-l,n.y-t.top)||!a(l,t.bottom-n.y)?!1:i<=o}function $(n,e,t){let i=performance.now(),o={point:n,time:i},{x:r,y:l}=n;if(e.add(o),e.length<2)return{x:r,y:l};let[a,s]=e.getFirstLast();if(!a||!s)return{x:r,y:l};let d=(s.time-a.time)*.001;if(d===0)return{x:r,y:l};let c=s.point.x-a.point.x,h=s.point.y-a.point.y,y=c/d,b=h/d,L=t*.001,w=r+y*L,V=l+b*L;return{x:w,y:V}}var F=class extends u{constructor(t){super(t.dependencies);this.moduleName="MousePredictor";this.trajectoryPositions=t.trajectoryPositions}updatePointerState(t){let i={x:t.clientX,y:t.clientY};this.trajectoryPositions.currentPoint=i,this.settings.enableMousePrediction?this.trajectoryPositions.predictedPoint=$(i,this.trajectoryPositions.positions,this.settings.trajectoryPredictionTime):this.trajectoryPositions.predictedPoint=i}processMouseMovement(t){this.updatePointerState(t);let i=this.settings.enableMousePrediction,o=this.trajectoryPositions.currentPoint;for(let r of this.elements.values()){if(!r.isIntersectingWithViewport||!r.callbackInfo.isCallbackActive||r.callbackInfo.isRunningCallback)continue;let l=r.elementBounds.expandedRect;if(i)I(o,this.trajectoryPositions.predictedPoint,l)&&this.callCallback(r,{kind:"mouse",subType:"trajectory"});else if(M(o,l)){this.callCallback(r,{kind:"mouse",subType:"hover"});return}}this.emit({type:"mouseTrajectoryUpdate",predictionEnabled:i,trajectoryPositions:this.trajectoryPositions})}onDisconnect(){}onConnect(){}};function Y(n,e){let i=e.top-n.top,o=e.left-n.left;return i<-1?"down":i>1?"up":o<-1?"right":o>1?"left":"none"}function G(n,e,t){let{x:i,y:o}=n,r={x:i,y:o};switch(e){case"down":r.y+=t;break;case"up":r.y-=t;break;case"left":r.x-=t;break;case"right":r.x+=t;break;case"none":break;default:}return r}var D=class extends u{constructor(t){super(t.dependencies);this.moduleName="ScrollPredictor";this.predictedScrollPoint=null;this.scrollDirection=null;this.onDisconnect=()=>this.resetScrollProps();this.trajectoryPositions=t.trajectoryPositions}resetScrollProps(){this.scrollDirection=null,this.predictedScrollPoint=null}handleScrollPrefetch(t,i){!t.isIntersectingWithViewport||t.callbackInfo.isRunningCallback||!t.callbackInfo.isCallbackActive||(this.scrollDirection=this.scrollDirection??Y(t.elementBounds.originalRect,i),this.scrollDirection!=="none"&&(this.predictedScrollPoint=this.predictedScrollPoint??G(this.trajectoryPositions.currentPoint,this.scrollDirection,this.settings.scrollMargin),I(this.trajectoryPositions.currentPoint,this.predictedScrollPoint,t.elementBounds.expandedRect)&&this.callCallback(t,{kind:"scroll",subType:this.scrollDirection}),this.emit({type:"scrollTrajectoryUpdate",currentPoint:this.trajectoryPositions.currentPoint,predictedPoint:this.predictedScrollPoint,scrollDirection:this.scrollDirection})))}onConnect(){}};import{tabbable as ie}from"tabbable";function X(n,e,t,i){if(e!==null&&e>-1){let o=n?e-1:e+1;if(o>=0&&o<t.length&&t[o]===i)return o}return t.findIndex(o=>o===i)}var _=class extends u{constructor(t){super(t);this.moduleName="TabPredictor";this.lastKeyDown=null;this.tabbableElementsCache=[];this.lastFocusedIndex=null;this.handleKeyDown=t=>{t.key==="Tab"&&(this.lastKeyDown=t)};this.handleFocusIn=t=>{if(!this.lastKeyDown)return;let i=t.target;if(!(i instanceof HTMLElement))return;(!this.tabbableElementsCache.length||this.lastFocusedIndex===-1)&&(this.devLog("Caching tabbable elements"),this.tabbableElementsCache=ie(document.documentElement));let o=this.lastKeyDown.shiftKey,r=X(o,this.lastFocusedIndex,this.tabbableElementsCache,i);this.lastFocusedIndex=r,this.lastKeyDown=null;let l=[],a=this.settings.tabOffset,s=this.elements;for(let d=0;d<=a;d++){let c=o?r-d:r+d,h=this.tabbableElementsCache[c];h&&h instanceof Element&&s.has(h)&&l.push(h)}for(let d of l){let c=s.get(d);c&&!c.callbackInfo.isRunningCallback&&c.callbackInfo.isCallbackActive&&this.callCallback(c,{kind:"tab",subType:o?"reverse":"forwards"})}}}invalidateCache(){this.tabbableElementsCache.length&&this.devLog("Invalidating tabbable elements cache"),this.tabbableElementsCache=[],this.lastFocusedIndex=null}onConnect(){typeof document>"u"||(this.createAbortController(),document.addEventListener("keydown",this.handleKeyDown,{signal:this.abortController?.signal,passive:!0}),document.addEventListener("focusin",this.handleFocusIn,{signal:this.abortController?.signal,passive:!0}))}onDisconnect(){this.tabbableElementsCache=[],this.lastFocusedIndex=null,this.lastKeyDown=null}};import{PositionObserver as ne}from"position-observer";var R=class{constructor(e){this.head=0;this.count=0;if(e<=0)throw new Error("CircularBuffer capacity must be greater than 0");this.capacity=e,this.buffer=new Array(e)}add(e){this.buffer[this.head]=e,this.head=(this.head+1)%this.capacity,this.count<this.capacity&&this.count++}getFirst(){if(this.count!==0)return this.count<this.capacity?this.buffer[0]:this.buffer[this.head]}getLast(){if(this.count!==0){if(this.count<this.capacity)return this.buffer[this.count-1];{let e=(this.head-1+this.capacity)%this.capacity;return this.buffer[e]}}}getFirstLast(){if(this.count===0)return[void 0,void 0];if(this.count===1){let i=this.count<this.capacity?this.buffer[0]:this.buffer[this.head];return[i,i]}let e=this.getFirst(),t=this.getLast();return[e,t]}resize(e){if(e<=0)throw new Error("CircularBuffer capacity must be greater than 0");if(e===this.capacity)return;let t=this.getAllItems();if(this.capacity=e,this.buffer=new Array(e),this.head=0,this.count=0,t.length>e){let i=t.slice(-e);for(let o of i)this.add(o)}else for(let i of t)this.add(i)}getAllItems(){if(this.count===0)return[];let e=new Array(this.count);if(this.count<this.capacity)for(let t=0;t<this.count;t++)e[t]=this.buffer[t];else{let t=this.head;for(let i=0;i<this.capacity;i++){let o=(t+i)%this.capacity;e[i]=this.buffer[o]}}return e}clear(){this.head=0,this.count=0}get length(){return this.count}get size(){return this.capacity}get isFull(){return this.count===this.capacity}get isEmpty(){return this.count===0}};var m=class extends u{constructor(t){super(t);this.moduleName="DesktopHandler";this.positionObserver=null;this.trajectoryPositions={positions:new R(8),currentPoint:{x:0,y:0},predictedPoint:{x:0,y:0}};this.handlePositionChange=t=>{let i=this.settings.enableScrollPrediction;for(let o of t){let r=this.elements.get(o.target);r&&(i?this.scrollPredictor?.handleScrollPrefetch(r,o.boundingClientRect):this.checkForMouseHover(r),this.handlePositionChangeDataUpdates(r,o))}i&&this.scrollPredictor?.resetScrollProps()};this.checkForMouseHover=t=>{M(this.trajectoryPositions.currentPoint,t.elementBounds.expandedRect)&&this.callCallback(t,{kind:"mouse",subType:"hover"})};this.handlePositionChangeDataUpdates=(t,i)=>{let o=[],r=i.isIntersecting;t.isIntersectingWithViewport!==r&&(o.push("visibility"),t.isIntersectingWithViewport=r),r&&(o.push("bounds"),t.elementBounds={hitSlop:t.elementBounds.hitSlop,originalRect:i.boundingClientRect,expandedRect:P(i.boundingClientRect,t.elementBounds.hitSlop)}),o.length&&this.emit({type:"elementDataUpdated",elementData:t,updatedProps:o})};this.processMouseMovement=t=>this.mousePredictor.processMouseMovement(t);this.invalidateTabCache=()=>this.tabPredictor?.invalidateCache();this.observeElement=t=>this.positionObserver?.observe(t);this.unobserveElement=t=>this.positionObserver?.unobserve(t);this.connectTabPredictor=()=>this.tabPredictor.connect();this.connectScrollPredictor=()=>this.scrollPredictor.connect();this.connectMousePredictor=()=>this.mousePredictor.connect();this.disconnectTabPredictor=()=>this.tabPredictor.disconnect();this.disconnectScrollPredictor=()=>this.scrollPredictor.disconnect();this.disconnectMousePredictor=()=>this.mousePredictor.disconnect();this.tabPredictor=new _(t),this.scrollPredictor=new D({dependencies:t,trajectoryPositions:this.trajectoryPositions}),this.mousePredictor=new F({dependencies:t,trajectoryPositions:this.trajectoryPositions})}onConnect(){this.settings.enableTabPrediction&&this.connectTabPredictor(),this.settings.enableScrollPrediction&&this.connectScrollPredictor(),this.connectMousePredictor(),this.positionObserver=new ne(this.handlePositionChange);let t=["mouse"];this.settings.enableTabPrediction&&t.push("tab"),this.settings.enableScrollPrediction&&t.push("scroll"),this.devLog(`Connected predictors: [${t.join(", ")}] and PositionObserver`);for(let i of this.elements.keys())this.positionObserver.observe(i)}onDisconnect(){this.disconnectMousePredictor(),this.disconnectTabPredictor(),this.disconnectScrollPredictor(),this.positionObserver?.disconnect(),this.positionObserver=null}};var k=class extends u{constructor(t){super(t);this.moduleName="ViewportPredictor";this.intersectionObserver=null;this.onConnect=()=>this.intersectionObserver=new IntersectionObserver(this.handleViewportEnter);this.observeElement=t=>this.intersectionObserver?.observe(t);this.unobserveElement=t=>this.intersectionObserver?.unobserve(t);this.handleViewportEnter=t=>{for(let i of t){if(!i.isIntersecting)continue;let o=this.elements.get(i.target);o&&(this.callCallback(o,{kind:"viewport"}),this.unobserveElement(i.target))}}}onDisconnect(){this.intersectionObserver?.disconnect(),this.intersectionObserver=null}};var O=class extends u{constructor(t){super(t);this.moduleName="TouchStartPredictor";this.onConnect=()=>this.createAbortController();this.onDisconnect=()=>{};this.handleTouchStart=t=>{let i=t.target,o=this.elements.get(i);o&&(this.callCallback(o,{kind:"touch"}),this.unobserveElement(i))}}observeElement(t){t instanceof HTMLElement&&t.addEventListener("touchstart",this.handleTouchStart,{signal:this.abortController?.signal})}unobserveElement(t){t instanceof HTMLElement&&t.removeEventListener("touchstart",this.handleTouchStart)}};var v=class extends u{constructor(t){super(t);this.moduleName="TouchDeviceHandler";this.predictor=null;this.onDisconnect=()=>{this.devLog("Disconnecting touch predictor"),this.predictor?.disconnect()};this.onConnect=()=>this.setTouchPredictor();this.observeElement=t=>this.predictor?.observeElement(t);this.unobserveElement=t=>this.predictor?.unobserveElement(t);this.viewportPredictor=new k(t),this.touchStartPredictor=new O(t),this.predictor=this.viewportPredictor}setTouchPredictor(){switch(this.predictor?.disconnect(),this.settings.touchDeviceStrategy){case"viewport":this.predictor=this.viewportPredictor,this.devLog("Connected touch strategy: viewport (ViewportPredictor)");break;case"onTouchStart":this.predictor=this.touchStartPredictor,this.devLog("Connected touch strategy: onTouchStart (TouchStartPredictor)");break;case"none":this.predictor=null,this.devLog('Touch strategy set to "none" - no predictor connected');return;default:this.settings.touchDeviceStrategy}this.predictor?.connect();for(let t of this.elements.keys())this.predictor?.observeElement(t)}};var T=class n{constructor(e){this.elements=new Map;this.idCounter=0;this.activeElementCount=0;this.isSetup=!1;this._globalCallbackHits={mouse:{hover:0,trajectory:0},tab:{forwards:0,reverse:0},scroll:{down:0,left:0,right:0,up:0},touch:0,viewport:0,total:0};this._globalSettings={debug:!1,enableManagerLogging:!1,enableMousePrediction:!0,enableScrollPrediction:!0,positionHistorySize:8,trajectoryPredictionTime:120,scrollMargin:150,defaultHitSlop:{top:0,left:0,right:0,bottom:0},enableTabPrediction:!0,tabOffset:2,touchDeviceStrategy:"onTouchStart",minimumConnectionType:"3g"};this.pendingPointerEvent=null;this.rafId=null;this.domObserver=null;this.eventListeners=new Map;this.currentDeviceStrategy=N()?"touch":"mouse";this.handlePointerMove=e=>{this.pendingPointerEvent=e,e.pointerType!=this.currentDeviceStrategy&&(this.emit({type:"deviceStrategyChanged",timestamp:Date.now(),newStrategy:e.pointerType,oldStrategy:this.currentDeviceStrategy}),this.setDeviceStrategy(this.currentDeviceStrategy=e.pointerType)),!this.rafId&&(this.rafId=requestAnimationFrame(()=>{if(this.handler instanceof v){this.rafId=null;return}this.pendingPointerEvent&&this.handler.processMouseMovement(this.pendingPointerEvent),this.rafId=null}))};this.handleDomMutations=e=>{if(!e.length)return;this.desktopHandler?.invalidateTabCache();let t=!1;for(let i=0;i<e.length;i++){let o=e[i];if(o.type==="childList"&&o.removedNodes.length>0){t=!0;break}}if(t)for(let i of this.elements.keys())i.isConnected||this.unregister(i,"disconnected")};e!==void 0&&this.initializeManagerSettings(e);let t={elements:this.elements,callCallback:this.callCallback.bind(this),emit:this.emit.bind(this),settings:this._globalSettings};this.desktopHandler=new m(t),this.touchDeviceHandler=new v(t),this.handler=this.currentDeviceStrategy==="mouse"?this.desktopHandler:this.touchDeviceHandler,this.devLog(`ForesightManager initialized with device strategy: ${this.currentDeviceStrategy}`),this.initializeGlobalListeners()}generateId(){return`foresight-${++this.idCounter}`}static initialize(e){return this.isInitiated||(n.manager=new n(e)),n.manager}addEventListener(e,t,i){if(i?.signal?.aborted)return()=>{};let o=this.eventListeners.get(e)??[];o.push(t),this.eventListeners.set(e,o),i?.signal?.addEventListener("abort",()=>this.removeEventListener(e,t))}removeEventListener(e,t){let i=this.eventListeners.get(e);if(!i)return;let o=i.indexOf(t);o>-1&&i.splice(o,1)}emit(e){let t=this.eventListeners.get(e.type)?.slice();if(t)for(let i=0;i<t.length;i++)try{t[i](e)}catch(o){console.error(`Error in ForesightManager event listener ${i} for ${e.type}:`,o)}}get getManagerData(){return{registeredElements:this.elements,globalSettings:this._globalSettings,globalCallbackHits:this._globalCallbackHits,eventListeners:this.eventListeners,currentDeviceStrategy:this.currentDeviceStrategy,activeElementCount:this.activeElementCount}}static get isInitiated(){return!!n.manager}static get instance(){return this.initialize()}get registeredElements(){return this.elements}register({element:e,callback:t,hitSlop:i,name:o,meta:r,reactivateAfter:l}){let{isTouchDevice:a,isLimitedConnection:s,shouldRegister:d}=K();if(!d)return{isLimitedConnection:s,isTouchDevice:a,isRegistered:!1,unregister:()=>{}};let c=this.elements.get(e);if(c)return c.registerCount++,{isLimitedConnection:s,isTouchDevice:a,isRegistered:!1,unregister:()=>{}};this.isSetup||this.initializeGlobalListeners();let h=e.getBoundingClientRect(),y=i?C(i):this._globalSettings.defaultHitSlop,b={id:this.generateId(),element:e,callback:t,elementBounds:{originalRect:h,expandedRect:P(h,y),hitSlop:y},name:o||e.id||"unnamed",isIntersectingWithViewport:z(h),registerCount:1,meta:r??{},callbackInfo:{callbackFiredCount:0,lastCallbackInvokedAt:void 0,lastCallbackCompletedAt:void 0,lastCallbackRuntime:void 0,lastCallbackStatus:void 0,lastCallbackErrorMessage:void 0,reactivateAfter:l??1/0,isCallbackActive:!0,isRunningCallback:!1,reactivateTimeoutId:void 0}};return this.elements.set(e,b),this.activeElementCount++,this.handler.observeElement(e),this.emit({type:"elementRegistered",timestamp:Date.now(),elementData:b}),{isTouchDevice:a,isLimitedConnection:s,isRegistered:!0,unregister:()=>{this.unregister(e)}}}unregister(e,t){let i=this.elements.get(e);if(!i)return;this.clearReactivateTimeout(i),this.handler.unobserveElement(e),this.elements.delete(e),i.callbackInfo.isCallbackActive&&this.activeElementCount--;let o=this.elements.size===0&&this.isSetup;o&&(this.devLog("All elements unregistered, removing global listeners"),this.removeGlobalListeners()),i&&this.emit({type:"elementUnregistered",elementData:i,timestamp:Date.now(),unregisterReason:t??"by user",wasLastRegisteredElement:o})}updateHitCounters(e){switch(e.kind){case"mouse":this._globalCallbackHits.mouse[e.subType]++;break;case"tab":this._globalCallbackHits.tab[e.subType]++;break;case"scroll":this._globalCallbackHits.scroll[e.subType]++;break;case"touch":this._globalCallbackHits.touch++;break;case"viewport":this._globalCallbackHits.viewport++;break;default:}this._globalCallbackHits.total++}reactivate(e){let t=this.elements.get(e);t&&(this.isSetup||this.initializeGlobalListeners(),this.clearReactivateTimeout(t),t.callbackInfo.isRunningCallback||(t.callbackInfo.isCallbackActive=!0,this.activeElementCount++,this.handler.observeElement(e),this.emit({type:"elementReactivated",elementData:t,timestamp:Date.now()})))}clearReactivateTimeout(e){clearTimeout(e.callbackInfo.reactivateTimeoutId),e.callbackInfo.reactivateTimeoutId=void 0}makeElementUnactive(e){e.callbackInfo.callbackFiredCount++,e.callbackInfo.lastCallbackInvokedAt=Date.now(),e.callbackInfo.isRunningCallback=!0,this.clearReactivateTimeout(e)}callCallback(e,t){if(e.callbackInfo.isRunningCallback||!e.callbackInfo.isCallbackActive)return;this.makeElementUnactive(e),(async()=>{this.updateHitCounters(t),this.emit({type:"callbackInvoked",timestamp:Date.now(),elementData:e,hitType:t});let o=performance.now(),r,l=null;try{await e.callback(),r="success"}catch(s){l=s instanceof Error?s.message:String(s),r="error",console.error(`Error in callback for element ${e.name}:`,s)}e.callbackInfo.lastCallbackCompletedAt=Date.now(),e.callbackInfo.isRunningCallback=!1,e.callbackInfo.isCallbackActive=!1,this.activeElementCount--,this.handler.unobserveElement(e.element),e.callbackInfo.reactivateAfter!==1/0&&(e.callbackInfo.reactivateTimeoutId=setTimeout(()=>{this.reactivate(e.element)},e.callbackInfo.reactivateAfter));let a=this.activeElementCount===0;a&&(this.devLog("All elements unactivated, removing global listeners"),this.removeGlobalListeners()),this.emit({type:"callbackCompleted",timestamp:Date.now(),elementData:e,hitType:t,elapsed:e.callbackInfo.lastCallbackRuntime=performance.now()-o,status:e.callbackInfo.lastCallbackStatus=r,errorMessage:e.callbackInfo.lastCallbackErrorMessage=l,wasLastActiveElement:a})})()}setDeviceStrategy(e){let t=this.handler instanceof m?"mouse":"touch";t!==e&&this.devLog(`Switching device strategy from ${t} to ${e}`),this.handler.disconnect(),this.handler=e==="mouse"?this.desktopHandler:this.touchDeviceHandler,this.handler.connect()}initializeGlobalListeners(){this.isSetup||typeof document>"u"||(this.devLog("Initializing global listeners (pointermove, MutationObserver)"),this.setDeviceStrategy(this.currentDeviceStrategy),document.addEventListener("pointermove",this.handlePointerMove),this.domObserver=new MutationObserver(this.handleDomMutations),this.domObserver.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1}),this.isSetup=!0)}removeGlobalListeners(){typeof document>"u"||(this.isSetup=!1,this.domObserver?.disconnect(),this.domObserver=null,document.removeEventListener("pointermove",this.handlePointerMove),this.handler.disconnect(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.pendingPointerEvent=null)}forceUpdateAllElementBounds(){for(let[,e]of this.elements)e.isIntersectingWithViewport&&this.forceUpdateElementBounds(e)}forceUpdateElementBounds(e){let t=e.element.getBoundingClientRect(),i=P(t,e.elementBounds.hitSlop);if(!A(i,e.elementBounds.expandedRect)){let o={...e,elementBounds:{...e.elementBounds,originalRect:t,expandedRect:i}};this.elements.set(e.element,o),this.emit({type:"elementDataUpdated",elementData:o,updatedProps:["bounds"]})}}initializeManagerSettings(e){this.updateNumericSettings(e.trajectoryPredictionTime,"trajectoryPredictionTime",10,200),this.updateNumericSettings(e.positionHistorySize,"positionHistorySize",2,30),this.updateNumericSettings(e.scrollMargin,"scrollMargin",30,300),this.updateNumericSettings(e.tabOffset,"tabOffset",0,20),this.updateBooleanSetting(e.enableMousePrediction,"enableMousePrediction"),this.updateBooleanSetting(e.enableScrollPrediction,"enableScrollPrediction"),this.updateBooleanSetting(e.enableTabPrediction,"enableTabPrediction"),this.updateBooleanSetting(e.enableManagerLogging,"enableManagerLogging"),e.defaultHitSlop!==void 0&&(this._globalSettings.defaultHitSlop=C(e.defaultHitSlop)),e.touchDeviceStrategy!==void 0&&(this._globalSettings.touchDeviceStrategy=e.touchDeviceStrategy),e.minimumConnectionType!==void 0&&(this._globalSettings.minimumConnectionType=e.minimumConnectionType),e.debug!==void 0&&(this._globalSettings.debug=e.debug)}updateNumericSettings(e,t,i,o){return H(e,this._globalSettings[t])?(this._globalSettings[t]=g(e,i,o,t),!0):!1}updateBooleanSetting(e,t){return H(e,this._globalSettings[t])?(this._globalSettings[t]=e,!0):!1}alterGlobalSettings(e){let t=[],i=this._globalSettings.trajectoryPredictionTime;this.updateNumericSettings(e?.trajectoryPredictionTime,"trajectoryPredictionTime",10,200)&&t.push({setting:"trajectoryPredictionTime",oldValue:i,newValue:this._globalSettings.trajectoryPredictionTime});let r=this._globalSettings.positionHistorySize;this.updateNumericSettings(e?.positionHistorySize,"positionHistorySize",2,30)&&(t.push({setting:"positionHistorySize",oldValue:r,newValue:this._globalSettings.positionHistorySize}),this.desktopHandler.trajectoryPositions.positions.resize(this._globalSettings.positionHistorySize));let a=this._globalSettings.scrollMargin;if(this.updateNumericSettings(e?.scrollMargin,"scrollMargin",30,300)){let p=this._globalSettings.scrollMargin;t.push({setting:"scrollMargin",oldValue:a,newValue:p})}let d=this._globalSettings.tabOffset;this.updateNumericSettings(e?.tabOffset,"tabOffset",0,20)&&(this._globalSettings.tabOffset=g(e.tabOffset,0,20,"tabOffset"),t.push({setting:"tabOffset",oldValue:d,newValue:this._globalSettings.tabOffset}));let h=this._globalSettings.enableMousePrediction;this.updateBooleanSetting(e?.enableMousePrediction,"enableMousePrediction")&&t.push({setting:"enableMousePrediction",oldValue:h,newValue:this._globalSettings.enableMousePrediction});let b=this._globalSettings.enableScrollPrediction;this.updateBooleanSetting(e?.enableScrollPrediction,"enableScrollPrediction")&&(this.handler instanceof m&&(this._globalSettings.enableScrollPrediction?this.handler.connectScrollPredictor():this.handler.disconnectScrollPredictor()),t.push({setting:"enableScrollPrediction",oldValue:b,newValue:this._globalSettings.enableScrollPrediction}));let w=this._globalSettings.enableTabPrediction;if(this.updateBooleanSetting(e?.enableTabPrediction,"enableTabPrediction")&&(this.handler instanceof m&&(this._globalSettings.enableTabPrediction?this.handler.connectTabPredictor():this.handler.disconnectTabPredictor()),t.push({setting:"enableTabPrediction",oldValue:w,newValue:this._globalSettings.enableTabPrediction})),e?.defaultHitSlop!==void 0){let p=this._globalSettings.defaultHitSlop,f=C(e.defaultHitSlop);A(p,f)||(this._globalSettings.defaultHitSlop=f,t.push({setting:"defaultHitSlop",oldValue:p,newValue:f}),this.forceUpdateAllElementBounds())}if(e?.touchDeviceStrategy!==void 0){let p=this._globalSettings.touchDeviceStrategy,f=e.touchDeviceStrategy;this._globalSettings.touchDeviceStrategy=f,t.push({setting:"touchDeviceStrategy",oldValue:p,newValue:f}),this.handler instanceof v&&this.handler.setTouchPredictor()}if(e?.minimumConnectionType!==void 0){let p=this._globalSettings.minimumConnectionType;this._globalSettings.minimumConnectionType=e.minimumConnectionType,t.push({setting:"minimumConnectionType",oldValue:p,newValue:this._globalSettings.minimumConnectionType})}t.length>0&&this.emit({type:"managerSettingsChanged",timestamp:Date.now(),managerData:this.getManagerData,updatedSettings:t})}devLog(e){this._globalSettings.enableManagerLogging&&console.log(`%c\u{1F6E0}\uFE0F ForesightManager: ${e}`,"color: #16a34a; font-weight: bold;")}};export{T as ForesightManager};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.foresight",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "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.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,8 +45,8 @@
45
45
  "llmsFull": "https://foresightjs.com/llms-full.txt",
46
46
  "devDependencies": {
47
47
  "@testing-library/dom": "^10.4.1",
48
- "@testing-library/jest-dom": "^6.6.4",
49
- "@types/node": "^24.1.0",
48
+ "@testing-library/jest-dom": "^6.7.0",
49
+ "@types/node": "^24.3.0",
50
50
  "@vitest/coverage-v8": "3.2.4",
51
51
  "@vitest/ui": "^3.2.4",
52
52
  "happy-dom": "^18.0.1",
@@ -57,7 +57,7 @@
57
57
  "vitest": "^3.2.4"
58
58
  },
59
59
  "dependencies": {
60
- "position-observer": "^1.0.1",
60
+ "position-observer": "^1.0.3",
61
61
  "tabbable": "^6.2.0"
62
62
  },
63
63
  "scripts": {