react-global-tracking 0.1.1 → 0.1.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/dist/index.cjs CHANGED
@@ -72,7 +72,6 @@ function getFiberFromElement(element) {
72
72
  if (cachedKey !== null) {
73
73
  const fiber = element[cachedKey];
74
74
  if (fiber != null) return fiber;
75
- return null;
76
75
  }
77
76
  for (const key of Object.keys(element)) for (const prefix of FIBER_PREFIXES) if (key.startsWith(prefix)) {
78
77
  cachedKey = key;
@@ -108,7 +107,7 @@ function getComponentName(fiber) {
108
107
  }
109
108
 
110
109
  //#endregion
111
- //#region src/utils/safe-matches.ts
110
+ //#region src/utils/safe-selector.ts
112
111
  function safeMatches(element, selector) {
113
112
  try {
114
113
  return element.matches(selector);
@@ -116,6 +115,13 @@ function safeMatches(element, selector) {
116
115
  return false;
117
116
  }
118
117
  }
118
+ function safeClosest(element, selector) {
119
+ try {
120
+ return element.closest(selector) !== null;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
119
125
 
120
126
  //#endregion
121
127
  //#region src/filter/filter-engine.ts
@@ -158,10 +164,10 @@ function findTrackableElement(params) {
158
164
  }
159
165
  }
160
166
  function findPointerTarget(target, ignoreSelectors, eventType) {
167
+ if (isIgnored(target, ignoreSelectors)) return null;
161
168
  let current = target;
162
169
  let depth = 0;
163
170
  while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {
164
- if (isIgnored(current, ignoreSelectors)) return null;
165
171
  if (isDisabled(current)) return null;
166
172
  const rawFiber = findInteractiveFiber(current, eventType);
167
173
  if (rawFiber !== void 0) return {
@@ -201,14 +207,14 @@ function findInteractiveFiber(el, eventType) {
201
207
  const fiber = resolveFiber(el);
202
208
  if (fiber !== null) {
203
209
  const props = fiber.memoizedProps;
204
- if (props !== null && props !== void 0) {
210
+ if (props != null) {
205
211
  for (const handler of handlers) if (typeof props[handler] === "function") return fiber;
206
212
  }
207
213
  }
208
214
  }
209
215
  }
210
216
  function isIgnored(element, ignoreSelectors) {
211
- return ignoreSelectors.some((selector) => safeMatches(element, selector));
217
+ return ignoreSelectors.some((selector) => safeClosest(element, selector));
212
218
  }
213
219
  function isDisabled(el) {
214
220
  return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
@@ -374,7 +380,7 @@ function createTracker(config) {
374
380
  pipeline.handleEvent(event);
375
381
  if (debug) {
376
382
  const lastEvent = pipeline.getLastEvent();
377
- if (lastEvent?.nativeEvent === event) console.debug("[react-auto-tracking]", lastEvent);
383
+ if (lastEvent?.nativeEvent === event) console.debug("[react-global-tracking]", lastEvent);
378
384
  }
379
385
  };
380
386
  document.addEventListener(eventType, handler, true);
package/dist/index.mjs CHANGED
@@ -70,7 +70,6 @@ function getFiberFromElement(element) {
70
70
  if (cachedKey !== null) {
71
71
  const fiber = element[cachedKey];
72
72
  if (fiber != null) return fiber;
73
- return null;
74
73
  }
75
74
  for (const key of Object.keys(element)) for (const prefix of FIBER_PREFIXES) if (key.startsWith(prefix)) {
76
75
  cachedKey = key;
@@ -106,7 +105,7 @@ function getComponentName(fiber) {
106
105
  }
107
106
 
108
107
  //#endregion
109
- //#region src/utils/safe-matches.ts
108
+ //#region src/utils/safe-selector.ts
110
109
  function safeMatches(element, selector) {
111
110
  try {
112
111
  return element.matches(selector);
@@ -114,6 +113,13 @@ function safeMatches(element, selector) {
114
113
  return false;
115
114
  }
116
115
  }
116
+ function safeClosest(element, selector) {
117
+ try {
118
+ return element.closest(selector) !== null;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
117
123
 
118
124
  //#endregion
119
125
  //#region src/filter/filter-engine.ts
@@ -156,10 +162,10 @@ function findTrackableElement(params) {
156
162
  }
157
163
  }
158
164
  function findPointerTarget(target, ignoreSelectors, eventType) {
165
+ if (isIgnored(target, ignoreSelectors)) return null;
159
166
  let current = target;
160
167
  let depth = 0;
161
168
  while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {
162
- if (isIgnored(current, ignoreSelectors)) return null;
163
169
  if (isDisabled(current)) return null;
164
170
  const rawFiber = findInteractiveFiber(current, eventType);
165
171
  if (rawFiber !== void 0) return {
@@ -199,14 +205,14 @@ function findInteractiveFiber(el, eventType) {
199
205
  const fiber = resolveFiber(el);
200
206
  if (fiber !== null) {
201
207
  const props = fiber.memoizedProps;
202
- if (props !== null && props !== void 0) {
208
+ if (props != null) {
203
209
  for (const handler of handlers) if (typeof props[handler] === "function") return fiber;
204
210
  }
205
211
  }
206
212
  }
207
213
  }
208
214
  function isIgnored(element, ignoreSelectors) {
209
- return ignoreSelectors.some((selector) => safeMatches(element, selector));
215
+ return ignoreSelectors.some((selector) => safeClosest(element, selector));
210
216
  }
211
217
  function isDisabled(el) {
212
218
  return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
@@ -372,7 +378,7 @@ function createTracker(config) {
372
378
  pipeline.handleEvent(event);
373
379
  if (debug) {
374
380
  const lastEvent = pipeline.getLastEvent();
375
- if (lastEvent?.nativeEvent === event) console.debug("[react-auto-tracking]", lastEvent);
381
+ if (lastEvent?.nativeEvent === event) console.debug("[react-global-tracking]", lastEvent);
376
382
  }
377
383
  };
378
384
  document.addEventListener(eventType, handler, true);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/filter/event-categories.ts","../src/extract/fiber.ts","../src/utils/safe-matches.ts","../src/filter/filter-engine.ts","../src/utils/debounce.ts","../src/utils/throttle.ts","../src/core/registry.ts","../src/core/pipeline.ts","../src/core/tracker.ts"],"sourcesContent":["export const EventCategory = {\n Pointer: 'pointer',\n Form: 'form',\n Ambient: 'ambient',\n} as const\n\nexport type EventCategory = (typeof EventCategory)[keyof typeof EventCategory]\n\n// Maps DOM event types to React handler prop names used to detect interactivity.\n// For click, we include mouse/pointer handlers because an element with only\n// onMouseDown (no onClick) is still interactive from a tracking perspective.\nconst EVENT_HANDLER_MAP: Readonly<Record<string, readonly string[]>> = {\n // Pointer — includes related mouse/pointer handlers to catch all interactive patterns\n click: ['onClick', 'onMouseDown', 'onMouseUp', 'onPointerDown', 'onPointerUp'],\n touchstart: ['onTouchStart'],\n touchend: ['onTouchEnd'],\n\n // Form\n input: ['onChange', 'onInput'],\n change: ['onChange'],\n focus: ['onFocus'],\n blur: ['onBlur'],\n submit: ['onSubmit'],\n\n // Ambient\n scroll: ['onScroll'],\n keydown: ['onKeyDown'],\n keyup: ['onKeyUp'],\n copy: ['onCopy'],\n paste: ['onPaste'],\n}\n\nconst CATEGORY_MAP: Readonly<Record<string, EventCategory>> = {\n click: EventCategory.Pointer,\n touchstart: EventCategory.Pointer,\n touchend: EventCategory.Pointer,\n\n input: EventCategory.Form,\n change: EventCategory.Form,\n focus: EventCategory.Form,\n blur: EventCategory.Form,\n submit: EventCategory.Form,\n\n scroll: EventCategory.Ambient,\n keydown: EventCategory.Ambient,\n keyup: EventCategory.Ambient,\n copy: EventCategory.Ambient,\n paste: EventCategory.Ambient,\n resize: EventCategory.Ambient,\n popstate: EventCategory.Ambient,\n hashchange: EventCategory.Ambient,\n}\n\nexport function getEventCategory(eventType: string): EventCategory {\n return CATEGORY_MAP[eventType] ?? EventCategory.Ambient\n}\n\nexport function getHandlersForEvent(eventType: string): readonly string[] {\n return EVENT_HANDLER_MAP[eventType] ?? []\n}\n","import type { FiberInfo } from '../types'\n\n// === Resolver: DOM element → raw fiber node ===\n\nconst FIBER_PREFIXES = ['__reactFiber$', '__reactInternalInstance$'] as const\nconst MAX_PARENT_DEPTH = 10\n\nlet cachedKey: string | null = null\n\nexport function resolveFiber(element: Element): object | null {\n let current: Element | null = element\n let depth = 0\n\n while (current !== null && depth <= MAX_PARENT_DEPTH) {\n const fiber = getFiberFromElement(current)\n if (fiber !== null) {\n return fiber\n }\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction getFiberFromElement(element: Element): object | null {\n if (cachedKey !== null) {\n const fiber = (element as any)[cachedKey]\n if (fiber != null) return fiber as object\n return null\n }\n\n for (const key of Object.keys(element)) {\n for (const prefix of FIBER_PREFIXES) {\n if (key.startsWith(prefix)) {\n cachedKey = key\n return (element as any)[key] as object\n }\n }\n }\n\n return null\n}\n\nexport function resetFiberKeyCache(): void {\n cachedKey = null\n}\n\n// === Extractor: raw fiber node → FiberInfo ===\n\nconst MAX_STACK_DEPTH = 50\n\ninterface FiberNode {\n type: unknown\n memoizedProps: Record<string, unknown> | null\n return: FiberNode | null\n}\n\nexport function extractFiberInfo(rawFiber: object | null): FiberInfo | null {\n if (rawFiber === null) return null\n\n const fiber = rawFiber as FiberNode\n return {\n componentName: findNearestComponentName(fiber),\n props: fiber.memoizedProps ?? {},\n }\n}\n\nfunction findNearestComponentName(fiber: FiberNode): string | null {\n let current: FiberNode | null = fiber.return\n let depth = 0\n\n while (current !== null && depth < MAX_STACK_DEPTH) {\n const name = getComponentName(current)\n if (name !== null) return name\n current = current.return\n depth++\n }\n\n return null\n}\n\nfunction getComponentName(fiber: FiberNode): string | null {\n const type = fiber.type\n if (typeof type === 'string') return null\n if (typeof type === 'function') {\n return (type as any).displayName ?? type.name ?? null\n }\n return null\n}\n","export function safeMatches(element: Element, selector: string): boolean {\n try {\n return element.matches(selector)\n } catch {\n return false\n }\n}\n","import type { FiberInfo } from '../types'\nimport { getEventCategory, getHandlersForEvent, EventCategory } from './event-categories'\nimport { resolveFiber, extractFiberInfo } from '../extract/fiber'\nimport { safeMatches } from '../utils/safe-matches'\n\nconst INTERACTIVE_TAGS = new Set([\n 'BUTTON',\n 'A',\n 'INPUT',\n 'SELECT',\n 'TEXTAREA',\n 'SUMMARY',\n 'DETAILS',\n])\n\nconst INTERACTIVE_ROLES = new Set([\n // Original widget roles\n 'button',\n 'link',\n 'menuitem',\n 'tab',\n 'checkbox',\n 'radio',\n 'combobox',\n 'listbox',\n 'option',\n 'switch',\n 'slider',\n 'spinbutton',\n // Composite widget variants\n 'menuitemcheckbox',\n 'menuitemradio',\n 'treeitem',\n 'gridcell',\n // Input widget roles\n 'textbox',\n 'searchbox',\n])\n\nconst MAX_ANCESTOR_DEPTH = 10\n\nexport interface FilterResult {\n readonly element: Element\n readonly fiber: FiberInfo | null\n}\n\ninterface FindTrackableParams {\n readonly target: Element\n readonly ignoreSelectors: readonly string[]\n readonly eventType: string\n}\n\nexport function findTrackableElement(params: FindTrackableParams): FilterResult | null {\n const { target, ignoreSelectors, eventType } = params\n const category = getEventCategory(eventType)\n\n switch (category) {\n case EventCategory.Pointer:\n return findPointerTarget(target, ignoreSelectors, eventType)\n case EventCategory.Form:\n return findFormTarget(target, ignoreSelectors)\n case EventCategory.Ambient:\n return findAmbientTarget(target, ignoreSelectors)\n }\n}\n\nfunction findPointerTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n eventType: string,\n): FilterResult | null {\n let current: Element | null = target\n let depth = 0\n\n while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {\n if (isIgnored(current, ignoreSelectors)) return null\n if (isDisabled(current)) return null\n\n const rawFiber = findInteractiveFiber(current, eventType)\n if (rawFiber !== undefined) {\n return { element: current, fiber: extractFiberInfo(rawFiber) }\n }\n\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction findFormTarget(target: Element, ignoreSelectors: readonly string[]): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n if (isDisabled(target)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\nfunction findAmbientTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\n/**\n * Returns the raw fiber if the element is interactive, or undefined if not.\n * null means \"interactive but no fiber found\" (semantic tag / ARIA role).\n */\nfunction findInteractiveFiber(el: Element, eventType: string): object | null | undefined {\n // 1. Semantic tag\n if (INTERACTIVE_TAGS.has(el.tagName)) return resolveFiber(el)\n\n // 2. ARIA role\n const role = el.getAttribute('role')\n if (role !== null && INTERACTIVE_ROLES.has(role)) return resolveFiber(el)\n\n // 3. React event handler via fiber\n const handlers = getHandlersForEvent(eventType)\n if (handlers.length > 0) {\n const fiber = resolveFiber(el)\n if (fiber !== null) {\n const props = (fiber as any).memoizedProps\n if (props !== null && props !== undefined) {\n for (const handler of handlers) {\n if (typeof props[handler] === 'function') return fiber\n }\n }\n }\n }\n\n return undefined\n}\n\nexport function isIgnored(element: Element, ignoreSelectors: readonly string[]): boolean {\n return ignoreSelectors.some((selector) => safeMatches(element, selector))\n}\n\nexport function isDisabled(el: Element): boolean {\n return el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true'\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface DebouncedFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): DebouncedFn<T> {\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n\n const debounced = (...args: Parameters<T>): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n }\n timeoutId = setTimeout(() => {\n timeoutId = null\n fn(...args)\n }, ms)\n }\n\n debounced.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n }\n\n return debounced\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface ThrottledFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(fn: T, ms: number): ThrottledFn<T> {\n let lastCallTime = 0\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n let lastArgs: Parameters<T> | null = null\n\n const throttled = (...args: Parameters<T>): void => {\n const now = Date.now()\n const elapsed = now - lastCallTime\n\n if (elapsed >= ms) {\n lastCallTime = now\n fn(...args)\n } else {\n lastArgs = args\n if (timeoutId === null) {\n timeoutId = setTimeout(() => {\n timeoutId = null\n lastCallTime = Date.now()\n if (lastArgs !== null) {\n fn(...lastArgs)\n lastArgs = null\n }\n }, ms - elapsed)\n }\n }\n }\n\n throttled.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n lastArgs = null\n }\n\n return throttled\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { debounce } from '../utils/debounce'\nimport { throttle } from '../utils/throttle'\nimport { safeMatches } from '../utils/safe-matches'\n\ninterface RegistryEntry {\n readonly eventType: string\n readonly wrappedCallback: TrackCallback & { cancel?: () => void }\n readonly options: ListenerOptions\n readonly unsubscribe: () => void\n}\n\nexport interface Registry {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n invoke(event: TrackEvent): void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport function createRegistry(): Registry {\n let entries: RegistryEntry[] = []\n\n function createEntry(\n eventType: string,\n callback: TrackCallback,\n options: ListenerOptions,\n ): RegistryEntry {\n const wrappedCallback = wrapCallback(callback, options)\n const entry: RegistryEntry = {\n eventType,\n wrappedCallback,\n options,\n unsubscribe: () => {\n wrappedCallback.cancel?.()\n entries = entries.filter((e) => e !== entry)\n },\n }\n return entry\n }\n\n return {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n const entry = createEntry(eventType, callback, options)\n entries = [...entries, entry]\n return entry.unsubscribe\n },\n\n invoke(event: TrackEvent): void {\n for (const entry of entries) {\n if (entry.eventType !== event.nativeEvent.type) continue\n\n if (entry.options.selector != null) {\n if (!safeMatches(event.targetElement, entry.options.selector)) {\n continue\n }\n }\n\n entry.wrappedCallback(event)\n\n // once: auto-unsubscribe after first fire\n if (entry.options.once === true) {\n entry.unsubscribe()\n }\n }\n },\n\n getEventTypes(): Set<string> {\n return new Set(entries.map((e) => e.eventType))\n },\n\n clear(): void {\n for (const entry of entries) {\n entry.wrappedCallback.cancel?.()\n }\n entries = []\n },\n }\n}\n\nfunction wrapCallback(\n callback: TrackCallback,\n options: ListenerOptions,\n): TrackCallback & { cancel?: () => void } {\n if (options.debounce != null) {\n return debounce(callback, options.debounce)\n }\n if (options.throttle != null) {\n return throttle(callback, options.throttle)\n }\n return callback\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { findTrackableElement } from '../filter/filter-engine'\nimport { createRegistry } from './registry'\n\nexport interface Pipeline {\n handleEvent(domEvent: Event): void\n getLastEvent(): TrackEvent | null\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport interface PipelineConfig {\n readonly ignoreSelectors: readonly string[]\n}\n\nexport function createPipeline(config: PipelineConfig): Pipeline {\n const registry = createRegistry()\n let lastEvent: TrackEvent | null = null\n\n return {\n handleEvent(domEvent: Event): void {\n const target = domEvent.target\n if (!(target instanceof Element)) return\n\n const result = findTrackableElement({\n target,\n ignoreSelectors: config.ignoreSelectors,\n eventType: domEvent.type,\n })\n if (result === null) return\n\n const trackEvent: TrackEvent = {\n nativeEvent: domEvent,\n targetElement: result.element,\n fiber: result.fiber,\n }\n\n registry.invoke(trackEvent)\n lastEvent = trackEvent\n },\n\n getLastEvent(): TrackEvent | null {\n return lastEvent\n },\n\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n return registry.add(eventType, callback, options)\n },\n\n getEventTypes(): Set<string> {\n return registry.getEventTypes()\n },\n\n clear(): void {\n registry.clear()\n },\n }\n}\n","import type { Tracker, TrackerConfig, TrackCallback, ListenerOptions } from '../types'\nimport { createPipeline } from './pipeline'\n\nexport function createTracker(config?: TrackerConfig): Tracker {\n const enabled = config?.enabled ?? true\n const ignoreSelectors = config?.ignoreSelectors ?? []\n const debug = config?.debug ?? false\n\n if (!enabled) {\n return {\n on: () => () => {},\n getLastEvent: () => null,\n destroy: () => {},\n }\n }\n\n const pipeline = createPipeline({ ignoreSelectors })\n const domListeners = new Map<string, (event: Event) => void>()\n let destroyed = false\n\n // Lazily attaches one capture-phase listener per event type on document.\n // Multiple on() calls for the same type share a single DOM listener;\n // the pipeline fans out to all registered callbacks internally.\n function ensureDomListener(eventType: string): void {\n if (domListeners.has(eventType)) return\n\n const handler = (event: Event): void => {\n pipeline.handleEvent(event)\n\n if (debug) {\n const lastEvent = pipeline.getLastEvent()\n if (lastEvent?.nativeEvent === event) {\n console.debug('[react-auto-tracking]', lastEvent)\n }\n }\n }\n\n document.addEventListener(eventType, handler, true)\n domListeners.set(eventType, handler)\n }\n\n function removeDomListener(eventType: string): void {\n const handler = domListeners.get(eventType)\n if (handler === undefined) return\n\n // Only remove if no more listeners for this type\n if (!pipeline.getEventTypes().has(eventType)) {\n document.removeEventListener(eventType, handler, true)\n domListeners.delete(eventType)\n }\n }\n\n return {\n on(eventType: string, callback: TrackCallback, options?: ListenerOptions): () => void {\n if (destroyed) return () => {}\n\n ensureDomListener(eventType)\n const unsub = pipeline.addListener(eventType, callback, options ?? {})\n\n return () => {\n unsub()\n removeDomListener(eventType)\n }\n },\n\n getLastEvent() {\n return pipeline.getLastEvent()\n },\n\n destroy(): void {\n if (destroyed) return\n destroyed = true\n\n pipeline.clear()\n\n for (const [eventType, handler] of domListeners) {\n document.removeEventListener(eventType, handler, true)\n }\n domListeners.clear()\n },\n }\n}\n"],"mappings":";AAAA,MAAa,gBAAgB;CAC3B,SAAS;CACT,MAAM;CACN,SAAS;CACV;AAOD,MAAM,oBAAiE;CAErE,OAAO;EAAC;EAAW;EAAe;EAAa;EAAiB;EAAc;CAC9E,YAAY,CAAC,eAAe;CAC5B,UAAU,CAAC,aAAa;CAGxB,OAAO,CAAC,YAAY,UAAU;CAC9B,QAAQ,CAAC,WAAW;CACpB,OAAO,CAAC,UAAU;CAClB,MAAM,CAAC,SAAS;CAChB,QAAQ,CAAC,WAAW;CAGpB,QAAQ,CAAC,WAAW;CACpB,SAAS,CAAC,YAAY;CACtB,OAAO,CAAC,UAAU;CAClB,MAAM,CAAC,SAAS;CAChB,OAAO,CAAC,UAAU;CACnB;AAED,MAAM,eAAwD;CAC5D,OAAO,cAAc;CACrB,YAAY,cAAc;CAC1B,UAAU,cAAc;CAExB,OAAO,cAAc;CACrB,QAAQ,cAAc;CACtB,OAAO,cAAc;CACrB,MAAM,cAAc;CACpB,QAAQ,cAAc;CAEtB,QAAQ,cAAc;CACtB,SAAS,cAAc;CACvB,OAAO,cAAc;CACrB,MAAM,cAAc;CACpB,OAAO,cAAc;CACrB,QAAQ,cAAc;CACtB,UAAU,cAAc;CACxB,YAAY,cAAc;CAC3B;AAED,SAAgB,iBAAiB,WAAkC;AACjE,QAAO,aAAa,cAAc,cAAc;;AAGlD,SAAgB,oBAAoB,WAAsC;AACxE,QAAO,kBAAkB,cAAc,EAAE;;;;;ACtD3C,MAAM,iBAAiB,CAAC,iBAAiB,2BAA2B;AACpE,MAAM,mBAAmB;AAEzB,IAAI,YAA2B;AAE/B,SAAgB,aAAa,SAAiC;CAC5D,IAAI,UAA0B;CAC9B,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,SAAS,kBAAkB;EACpD,MAAM,QAAQ,oBAAoB,QAAQ;AAC1C,MAAI,UAAU,KACZ,QAAO;AAET,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,oBAAoB,SAAiC;AAC5D,KAAI,cAAc,MAAM;EACtB,MAAM,QAAS,QAAgB;AAC/B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO;;AAGT,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,MAAK,MAAM,UAAU,eACnB,KAAI,IAAI,WAAW,OAAO,EAAE;AAC1B,cAAY;AACZ,SAAQ,QAAgB;;AAK9B,QAAO;;AAST,MAAM,kBAAkB;AAQxB,SAAgB,iBAAiB,UAA2C;AAC1E,KAAI,aAAa,KAAM,QAAO;CAE9B,MAAM,QAAQ;AACd,QAAO;EACL,eAAe,yBAAyB,MAAM;EAC9C,OAAO,MAAM,iBAAiB,EAAE;EACjC;;AAGH,SAAS,yBAAyB,OAAiC;CACjE,IAAI,UAA4B,MAAM;CACtC,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,QAAQ,iBAAiB;EAClD,MAAM,OAAO,iBAAiB,QAAQ;AACtC,MAAI,SAAS,KAAM,QAAO;AAC1B,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,iBAAiB,OAAiC;CACzD,MAAM,OAAO,MAAM;AACnB,KAAI,OAAO,SAAS,SAAU,QAAO;AACrC,KAAI,OAAO,SAAS,WAClB,QAAQ,KAAa,eAAe,KAAK,QAAQ;AAEnD,QAAO;;;;;ACxFT,SAAgB,YAAY,SAAkB,UAA2B;AACvE,KAAI;AACF,SAAO,QAAQ,QAAQ,SAAS;SAC1B;AACN,SAAO;;;;;;ACCX,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,oBAAoB,IAAI,IAAI;CAEhC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CACD,CAAC;AAEF,MAAM,qBAAqB;AAa3B,SAAgB,qBAAqB,QAAkD;CACrF,MAAM,EAAE,QAAQ,iBAAiB,cAAc;AAG/C,SAFiB,iBAAiB,UAAU,EAE5C;EACE,KAAK,cAAc,QACjB,QAAO,kBAAkB,QAAQ,iBAAiB,UAAU;EAC9D,KAAK,cAAc,KACjB,QAAO,eAAe,QAAQ,gBAAgB;EAChD,KAAK,cAAc,QACjB,QAAO,kBAAkB,QAAQ,gBAAgB;;;AAIvD,SAAS,kBACP,QACA,iBACA,WACqB;CACrB,IAAI,UAA0B;CAC9B,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,SAAS,oBAAoB;AACtD,MAAI,UAAU,SAAS,gBAAgB,CAAE,QAAO;AAChD,MAAI,WAAW,QAAQ,CAAE,QAAO;EAEhC,MAAM,WAAW,qBAAqB,SAAS,UAAU;AACzD,MAAI,aAAa,OACf,QAAO;GAAE,SAAS;GAAS,OAAO,iBAAiB,SAAS;GAAE;AAGhE,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,eAAe,QAAiB,iBAAyD;AAChG,KAAI,UAAU,QAAQ,gBAAgB,CAAE,QAAO;AAC/C,KAAI,WAAW,OAAO,CAAE,QAAO;AAC/B,QAAO;EAAE,SAAS;EAAQ,OAAO,iBAAiB,aAAa,OAAO,CAAC;EAAE;;AAG3E,SAAS,kBACP,QACA,iBACqB;AACrB,KAAI,UAAU,QAAQ,gBAAgB,CAAE,QAAO;AAC/C,QAAO;EAAE,SAAS;EAAQ,OAAO,iBAAiB,aAAa,OAAO,CAAC;EAAE;;;;;;AAO3E,SAAS,qBAAqB,IAAa,WAA8C;AAEvF,KAAI,iBAAiB,IAAI,GAAG,QAAQ,CAAE,QAAO,aAAa,GAAG;CAG7D,MAAM,OAAO,GAAG,aAAa,OAAO;AACpC,KAAI,SAAS,QAAQ,kBAAkB,IAAI,KAAK,CAAE,QAAO,aAAa,GAAG;CAGzE,MAAM,WAAW,oBAAoB,UAAU;AAC/C,KAAI,SAAS,SAAS,GAAG;EACvB,MAAM,QAAQ,aAAa,GAAG;AAC9B,MAAI,UAAU,MAAM;GAClB,MAAM,QAAS,MAAc;AAC7B,OAAI,UAAU,QAAQ,UAAU,QAC9B;SAAK,MAAM,WAAW,SACpB,KAAI,OAAO,MAAM,aAAa,WAAY,QAAO;;;;;AAS3D,SAAgB,UAAU,SAAkB,iBAA6C;AACvF,QAAO,gBAAgB,MAAM,aAAa,YAAY,SAAS,SAAS,CAAC;;AAG3E,SAAgB,WAAW,IAAsB;AAC/C,QAAO,GAAG,aAAa,WAAW,IAAI,GAAG,aAAa,gBAAgB,KAAK;;;;;ACnI7E,SAAgB,SAA6C,IAAO,IAA4B;CAC9F,IAAI,YAAkD;CAEtD,MAAM,aAAa,GAAG,SAA8B;AAClD,MAAI,cAAc,KAChB,cAAa,UAAU;AAEzB,cAAY,iBAAiB;AAC3B,eAAY;AACZ,MAAG,GAAG,KAAK;KACV,GAAG;;AAGR,WAAU,eAAqB;AAC7B,MAAI,cAAc,MAAM;AACtB,gBAAa,UAAU;AACvB,eAAY;;;AAIhB,QAAO;;;;;ACpBT,SAAgB,SAA6C,IAAO,IAA4B;CAC9F,IAAI,eAAe;CACnB,IAAI,YAAkD;CACtD,IAAI,WAAiC;CAErC,MAAM,aAAa,GAAG,SAA8B;EAClD,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,UAAU,MAAM;AAEtB,MAAI,WAAW,IAAI;AACjB,kBAAe;AACf,MAAG,GAAG,KAAK;SACN;AACL,cAAW;AACX,OAAI,cAAc,KAChB,aAAY,iBAAiB;AAC3B,gBAAY;AACZ,mBAAe,KAAK,KAAK;AACzB,QAAI,aAAa,MAAM;AACrB,QAAG,GAAG,SAAS;AACf,gBAAW;;MAEZ,KAAK,QAAQ;;;AAKtB,WAAU,eAAqB;AAC7B,MAAI,cAAc,MAAM;AACtB,gBAAa,UAAU;AACvB,eAAY;;AAEd,aAAW;;AAGb,QAAO;;;;;ACvBT,SAAgB,iBAA2B;CACzC,IAAI,UAA2B,EAAE;CAEjC,SAAS,YACP,WACA,UACA,SACe;EACf,MAAM,kBAAkB,aAAa,UAAU,QAAQ;EACvD,MAAM,QAAuB;GAC3B;GACA;GACA;GACA,mBAAmB;AACjB,oBAAgB,UAAU;AAC1B,cAAU,QAAQ,QAAQ,MAAM,MAAM,MAAM;;GAE/C;AACD,SAAO;;AAGT,QAAO;EACL,IAAI,WAAmB,UAAyB,SAAsC;GACpF,MAAM,QAAQ,YAAY,WAAW,UAAU,QAAQ;AACvD,aAAU,CAAC,GAAG,SAAS,MAAM;AAC7B,UAAO,MAAM;;EAGf,OAAO,OAAyB;AAC9B,QAAK,MAAM,SAAS,SAAS;AAC3B,QAAI,MAAM,cAAc,MAAM,YAAY,KAAM;AAEhD,QAAI,MAAM,QAAQ,YAAY,MAC5B;SAAI,CAAC,YAAY,MAAM,eAAe,MAAM,QAAQ,SAAS,CAC3D;;AAIJ,UAAM,gBAAgB,MAAM;AAG5B,QAAI,MAAM,QAAQ,SAAS,KACzB,OAAM,aAAa;;;EAKzB,gBAA6B;AAC3B,UAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC;;EAGjD,QAAc;AACZ,QAAK,MAAM,SAAS,QAClB,OAAM,gBAAgB,UAAU;AAElC,aAAU,EAAE;;EAEf;;AAGH,SAAS,aACP,UACA,SACyC;AACzC,KAAI,QAAQ,YAAY,KACtB,QAAO,SAAS,UAAU,QAAQ,SAAS;AAE7C,KAAI,QAAQ,YAAY,KACtB,QAAO,SAAS,UAAU,QAAQ,SAAS;AAE7C,QAAO;;;;;ACzET,SAAgB,eAAe,QAAkC;CAC/D,MAAM,WAAW,gBAAgB;CACjC,IAAI,YAA+B;AAEnC,QAAO;EACL,YAAY,UAAuB;GACjC,MAAM,SAAS,SAAS;AACxB,OAAI,EAAE,kBAAkB,SAAU;GAElC,MAAM,SAAS,qBAAqB;IAClC;IACA,iBAAiB,OAAO;IACxB,WAAW,SAAS;IACrB,CAAC;AACF,OAAI,WAAW,KAAM;GAErB,MAAM,aAAyB;IAC7B,aAAa;IACb,eAAe,OAAO;IACtB,OAAO,OAAO;IACf;AAED,YAAS,OAAO,WAAW;AAC3B,eAAY;;EAGd,eAAkC;AAChC,UAAO;;EAGT,YAAY,WAAmB,UAAyB,SAAsC;AAC5F,UAAO,SAAS,IAAI,WAAW,UAAU,QAAQ;;EAGnD,gBAA6B;AAC3B,UAAO,SAAS,eAAe;;EAGjC,QAAc;AACZ,YAAS,OAAO;;EAEnB;;;;;ACtDH,SAAgB,cAAc,QAAiC;CAC7D,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,kBAAkB,QAAQ,mBAAmB,EAAE;CACrD,MAAM,QAAQ,QAAQ,SAAS;AAE/B,KAAI,CAAC,QACH,QAAO;EACL,gBAAgB;EAChB,oBAAoB;EACpB,eAAe;EAChB;CAGH,MAAM,WAAW,eAAe,EAAE,iBAAiB,CAAC;CACpD,MAAM,+BAAe,IAAI,KAAqC;CAC9D,IAAI,YAAY;CAKhB,SAAS,kBAAkB,WAAyB;AAClD,MAAI,aAAa,IAAI,UAAU,CAAE;EAEjC,MAAM,WAAW,UAAuB;AACtC,YAAS,YAAY,MAAM;AAE3B,OAAI,OAAO;IACT,MAAM,YAAY,SAAS,cAAc;AACzC,QAAI,WAAW,gBAAgB,MAC7B,SAAQ,MAAM,yBAAyB,UAAU;;;AAKvD,WAAS,iBAAiB,WAAW,SAAS,KAAK;AACnD,eAAa,IAAI,WAAW,QAAQ;;CAGtC,SAAS,kBAAkB,WAAyB;EAClD,MAAM,UAAU,aAAa,IAAI,UAAU;AAC3C,MAAI,YAAY,OAAW;AAG3B,MAAI,CAAC,SAAS,eAAe,CAAC,IAAI,UAAU,EAAE;AAC5C,YAAS,oBAAoB,WAAW,SAAS,KAAK;AACtD,gBAAa,OAAO,UAAU;;;AAIlC,QAAO;EACL,GAAG,WAAmB,UAAyB,SAAuC;AACpF,OAAI,UAAW,cAAa;AAE5B,qBAAkB,UAAU;GAC5B,MAAM,QAAQ,SAAS,YAAY,WAAW,UAAU,WAAW,EAAE,CAAC;AAEtE,gBAAa;AACX,WAAO;AACP,sBAAkB,UAAU;;;EAIhC,eAAe;AACb,UAAO,SAAS,cAAc;;EAGhC,UAAgB;AACd,OAAI,UAAW;AACf,eAAY;AAEZ,YAAS,OAAO;AAEhB,QAAK,MAAM,CAAC,WAAW,YAAY,aACjC,UAAS,oBAAoB,WAAW,SAAS,KAAK;AAExD,gBAAa,OAAO;;EAEvB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/filter/event-categories.ts","../src/extract/fiber.ts","../src/utils/safe-selector.ts","../src/filter/filter-engine.ts","../src/utils/debounce.ts","../src/utils/throttle.ts","../src/core/registry.ts","../src/core/pipeline.ts","../src/core/tracker.ts"],"sourcesContent":["export const EventCategory = {\n Pointer: 'pointer',\n Form: 'form',\n Ambient: 'ambient',\n} as const\n\nexport type EventCategory = (typeof EventCategory)[keyof typeof EventCategory]\n\n// Maps DOM event types to React handler prop names used to detect interactivity.\n// For click, we include mouse/pointer handlers because an element with only\n// onMouseDown (no onClick) is still interactive from a tracking perspective.\nconst EVENT_HANDLER_MAP: Readonly<Record<string, readonly string[]>> = {\n // Pointer — includes related mouse/pointer handlers to catch all interactive patterns\n click: ['onClick', 'onMouseDown', 'onMouseUp', 'onPointerDown', 'onPointerUp'],\n touchstart: ['onTouchStart'],\n touchend: ['onTouchEnd'],\n\n // Form\n input: ['onChange', 'onInput'],\n change: ['onChange'],\n focus: ['onFocus'],\n blur: ['onBlur'],\n submit: ['onSubmit'],\n\n // Ambient\n scroll: ['onScroll'],\n keydown: ['onKeyDown'],\n keyup: ['onKeyUp'],\n copy: ['onCopy'],\n paste: ['onPaste'],\n}\n\nconst CATEGORY_MAP: Readonly<Record<string, EventCategory>> = {\n click: EventCategory.Pointer,\n touchstart: EventCategory.Pointer,\n touchend: EventCategory.Pointer,\n\n input: EventCategory.Form,\n change: EventCategory.Form,\n focus: EventCategory.Form,\n blur: EventCategory.Form,\n submit: EventCategory.Form,\n\n scroll: EventCategory.Ambient,\n keydown: EventCategory.Ambient,\n keyup: EventCategory.Ambient,\n copy: EventCategory.Ambient,\n paste: EventCategory.Ambient,\n resize: EventCategory.Ambient,\n popstate: EventCategory.Ambient,\n hashchange: EventCategory.Ambient,\n}\n\nexport function getEventCategory(eventType: string): EventCategory {\n return CATEGORY_MAP[eventType] ?? EventCategory.Ambient\n}\n\nexport function getHandlersForEvent(eventType: string): readonly string[] {\n return EVENT_HANDLER_MAP[eventType] ?? []\n}\n","import type { FiberInfo } from '../types'\n\n// === Resolver: DOM element → raw fiber node ===\n\nconst FIBER_PREFIXES = ['__reactFiber$', '__reactInternalInstance$'] as const\nconst MAX_PARENT_DEPTH = 10\n\nlet cachedKey: string | null = null\n\nexport function resolveFiber(element: Element): object | null {\n let current: Element | null = element\n let depth = 0\n\n while (current !== null && depth <= MAX_PARENT_DEPTH) {\n const fiber = getFiberFromElement(current)\n if (fiber !== null) {\n return fiber\n }\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction getFiberFromElement(element: Element): object | null {\n if (cachedKey !== null) {\n const fiber = (element as any)[cachedKey]\n if (fiber != null) return fiber as object\n }\n\n for (const key of Object.keys(element)) {\n for (const prefix of FIBER_PREFIXES) {\n if (key.startsWith(prefix)) {\n cachedKey = key\n return (element as any)[key] as object\n }\n }\n }\n\n return null\n}\n\nexport function resetFiberKeyCache(): void {\n cachedKey = null\n}\n\n// === Extractor: raw fiber node → FiberInfo ===\n\nconst MAX_STACK_DEPTH = 50\n\ninterface FiberNode {\n type: unknown\n memoizedProps: Record<string, unknown> | null\n return: FiberNode | null\n}\n\nexport function extractFiberInfo(rawFiber: object | null): FiberInfo | null {\n if (rawFiber === null) return null\n\n const fiber = rawFiber as FiberNode\n return {\n componentName: findNearestComponentName(fiber),\n props: fiber.memoizedProps ?? {},\n }\n}\n\nfunction findNearestComponentName(fiber: FiberNode): string | null {\n let current: FiberNode | null = fiber.return\n let depth = 0\n\n while (current !== null && depth < MAX_STACK_DEPTH) {\n const name = getComponentName(current)\n if (name !== null) return name\n current = current.return\n depth++\n }\n\n return null\n}\n\nfunction getComponentName(fiber: FiberNode): string | null {\n const type = fiber.type\n if (typeof type === 'string') return null\n if (typeof type === 'function') {\n return (type as any).displayName ?? type.name ?? null\n }\n return null\n}\n","export function safeMatches(element: Element, selector: string): boolean {\n try {\n return element.matches(selector)\n } catch {\n return false\n }\n}\n\nexport function safeClosest(element: Element, selector: string): boolean {\n try {\n return element.closest(selector) !== null\n } catch {\n return false\n }\n}\n","import type { FiberInfo } from '../types'\nimport { getEventCategory, getHandlersForEvent, EventCategory } from './event-categories'\nimport { resolveFiber, extractFiberInfo } from '../extract/fiber'\nimport { safeClosest } from '../utils/safe-selector'\n\nconst INTERACTIVE_TAGS = new Set([\n 'BUTTON',\n 'A',\n 'INPUT',\n 'SELECT',\n 'TEXTAREA',\n 'SUMMARY',\n 'DETAILS',\n])\n\nconst INTERACTIVE_ROLES = new Set([\n // Original widget roles\n 'button',\n 'link',\n 'menuitem',\n 'tab',\n 'checkbox',\n 'radio',\n 'combobox',\n 'listbox',\n 'option',\n 'switch',\n 'slider',\n 'spinbutton',\n // Composite widget variants\n 'menuitemcheckbox',\n 'menuitemradio',\n 'treeitem',\n 'gridcell',\n // Input widget roles\n 'textbox',\n 'searchbox',\n])\n\nconst MAX_ANCESTOR_DEPTH = 10\n\nexport interface FilterResult {\n readonly element: Element\n readonly fiber: FiberInfo | null\n}\n\ninterface FindTrackableParams {\n readonly target: Element\n readonly ignoreSelectors: readonly string[]\n readonly eventType: string\n}\n\nexport function findTrackableElement(params: FindTrackableParams): FilterResult | null {\n const { target, ignoreSelectors, eventType } = params\n const category = getEventCategory(eventType)\n\n switch (category) {\n case EventCategory.Pointer:\n return findPointerTarget(target, ignoreSelectors, eventType)\n case EventCategory.Form:\n return findFormTarget(target, ignoreSelectors)\n case EventCategory.Ambient:\n return findAmbientTarget(target, ignoreSelectors)\n }\n}\n\nfunction findPointerTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n eventType: string,\n): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n\n let current: Element | null = target\n let depth = 0\n\n while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {\n if (isDisabled(current)) return null\n\n const rawFiber = findInteractiveFiber(current, eventType)\n if (rawFiber !== undefined) {\n return { element: current, fiber: extractFiberInfo(rawFiber) }\n }\n\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction findFormTarget(target: Element, ignoreSelectors: readonly string[]): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n if (isDisabled(target)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\nfunction findAmbientTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\n/**\n * Returns the raw fiber if the element is interactive, or undefined if not.\n * null means \"interactive but no fiber found\" (semantic tag / ARIA role).\n */\nfunction findInteractiveFiber(el: Element, eventType: string): object | null | undefined {\n // 1. Semantic tag\n if (INTERACTIVE_TAGS.has(el.tagName)) return resolveFiber(el)\n\n // 2. ARIA role\n const role = el.getAttribute('role')\n if (role !== null && INTERACTIVE_ROLES.has(role)) return resolveFiber(el)\n\n // 3. React event handler via fiber\n const handlers = getHandlersForEvent(eventType)\n if (handlers.length > 0) {\n const fiber = resolveFiber(el)\n if (fiber !== null) {\n const props = (fiber as any).memoizedProps\n if (props != null) {\n for (const handler of handlers) {\n if (typeof props[handler] === 'function') return fiber\n }\n }\n }\n }\n\n return undefined\n}\n\nexport function isIgnored(element: Element, ignoreSelectors: readonly string[]): boolean {\n return ignoreSelectors.some((selector) => safeClosest(element, selector))\n}\n\nexport function isDisabled(el: Element): boolean {\n return el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true'\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface DebouncedFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): DebouncedFn<T> {\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n\n const debounced = (...args: Parameters<T>): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n }\n timeoutId = setTimeout(() => {\n timeoutId = null\n fn(...args)\n }, ms)\n }\n\n debounced.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n }\n\n return debounced\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface ThrottledFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(fn: T, ms: number): ThrottledFn<T> {\n let lastCallTime = 0\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n let lastArgs: Parameters<T> | null = null\n\n const throttled = (...args: Parameters<T>): void => {\n const now = Date.now()\n const elapsed = now - lastCallTime\n\n if (elapsed >= ms) {\n lastCallTime = now\n fn(...args)\n } else {\n lastArgs = args\n if (timeoutId === null) {\n timeoutId = setTimeout(() => {\n timeoutId = null\n lastCallTime = Date.now()\n if (lastArgs !== null) {\n fn(...lastArgs)\n lastArgs = null\n }\n }, ms - elapsed)\n }\n }\n }\n\n throttled.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n lastArgs = null\n }\n\n return throttled\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { debounce } from '../utils/debounce'\nimport { throttle } from '../utils/throttle'\nimport { safeMatches } from '../utils/safe-selector'\n\ninterface RegistryEntry {\n readonly eventType: string\n readonly wrappedCallback: TrackCallback & { cancel?: () => void }\n readonly options: ListenerOptions\n readonly unsubscribe: () => void\n}\n\nexport interface Registry {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n invoke(event: TrackEvent): void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport function createRegistry(): Registry {\n let entries: RegistryEntry[] = []\n\n function createEntry(\n eventType: string,\n callback: TrackCallback,\n options: ListenerOptions,\n ): RegistryEntry {\n const wrappedCallback = wrapCallback(callback, options)\n const entry: RegistryEntry = {\n eventType,\n wrappedCallback,\n options,\n unsubscribe: () => {\n wrappedCallback.cancel?.()\n entries = entries.filter((e) => e !== entry)\n },\n }\n return entry\n }\n\n return {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n const entry = createEntry(eventType, callback, options)\n entries = [...entries, entry]\n return entry.unsubscribe\n },\n\n invoke(event: TrackEvent): void {\n for (const entry of entries) {\n if (entry.eventType !== event.nativeEvent.type) continue\n\n if (entry.options.selector != null) {\n if (!safeMatches(event.targetElement, entry.options.selector)) {\n continue\n }\n }\n\n entry.wrappedCallback(event)\n\n // once: auto-unsubscribe after first fire\n if (entry.options.once === true) {\n entry.unsubscribe()\n }\n }\n },\n\n getEventTypes(): Set<string> {\n return new Set(entries.map((e) => e.eventType))\n },\n\n clear(): void {\n for (const entry of entries) {\n entry.wrappedCallback.cancel?.()\n }\n entries = []\n },\n }\n}\n\nfunction wrapCallback(\n callback: TrackCallback,\n options: ListenerOptions,\n): TrackCallback & { cancel?: () => void } {\n if (options.debounce != null) {\n return debounce(callback, options.debounce)\n }\n if (options.throttle != null) {\n return throttle(callback, options.throttle)\n }\n return callback\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { findTrackableElement } from '../filter/filter-engine'\nimport { createRegistry } from './registry'\n\nexport interface Pipeline {\n handleEvent(domEvent: Event): void\n getLastEvent(): TrackEvent | null\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport interface PipelineConfig {\n readonly ignoreSelectors: readonly string[]\n}\n\nexport function createPipeline(config: PipelineConfig): Pipeline {\n const registry = createRegistry()\n let lastEvent: TrackEvent | null = null\n\n return {\n handleEvent(domEvent: Event): void {\n const target = domEvent.target\n if (!(target instanceof Element)) return\n\n const result = findTrackableElement({\n target,\n ignoreSelectors: config.ignoreSelectors,\n eventType: domEvent.type,\n })\n if (result === null) return\n\n const trackEvent: TrackEvent = {\n nativeEvent: domEvent,\n targetElement: result.element,\n fiber: result.fiber,\n }\n\n registry.invoke(trackEvent)\n lastEvent = trackEvent\n },\n\n getLastEvent(): TrackEvent | null {\n return lastEvent\n },\n\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n return registry.add(eventType, callback, options)\n },\n\n getEventTypes(): Set<string> {\n return registry.getEventTypes()\n },\n\n clear(): void {\n registry.clear()\n },\n }\n}\n","import type { Tracker, TrackerConfig, TrackCallback, ListenerOptions } from '../types'\nimport { createPipeline } from './pipeline'\n\nexport function createTracker(config?: TrackerConfig): Tracker {\n const enabled = config?.enabled ?? true\n const ignoreSelectors = config?.ignoreSelectors ?? []\n const debug = config?.debug ?? false\n\n if (!enabled) {\n return {\n on: () => () => {},\n getLastEvent: () => null,\n destroy: () => {},\n }\n }\n\n const pipeline = createPipeline({ ignoreSelectors })\n const domListeners = new Map<string, (event: Event) => void>()\n let destroyed = false\n\n // Lazily attaches one capture-phase listener per event type on document.\n // Multiple on() calls for the same type share a single DOM listener;\n // the pipeline fans out to all registered callbacks internally.\n function ensureDomListener(eventType: string): void {\n if (domListeners.has(eventType)) return\n\n const handler = (event: Event): void => {\n pipeline.handleEvent(event)\n\n if (debug) {\n const lastEvent = pipeline.getLastEvent()\n if (lastEvent?.nativeEvent === event) {\n console.debug('[react-global-tracking]', lastEvent)\n }\n }\n }\n\n document.addEventListener(eventType, handler, true)\n domListeners.set(eventType, handler)\n }\n\n function removeDomListener(eventType: string): void {\n const handler = domListeners.get(eventType)\n if (handler === undefined) return\n\n // Only remove if no more listeners for this type\n if (!pipeline.getEventTypes().has(eventType)) {\n document.removeEventListener(eventType, handler, true)\n domListeners.delete(eventType)\n }\n }\n\n return {\n on(eventType: string, callback: TrackCallback, options?: ListenerOptions): () => void {\n if (destroyed) return () => {}\n\n ensureDomListener(eventType)\n const unsub = pipeline.addListener(eventType, callback, options ?? {})\n\n return () => {\n unsub()\n removeDomListener(eventType)\n }\n },\n\n getLastEvent() {\n return pipeline.getLastEvent()\n },\n\n destroy(): void {\n if (destroyed) return\n destroyed = true\n\n pipeline.clear()\n\n for (const [eventType, handler] of domListeners) {\n document.removeEventListener(eventType, handler, true)\n }\n domListeners.clear()\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-global-tracking",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Global user interaction tracking for React.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -21,17 +21,6 @@
21
21
  "files": [
22
22
  "dist"
23
23
  ],
24
- "scripts": {
25
- "build": "tsdown",
26
- "test": "vitest run",
27
- "test:watch": "vitest",
28
- "test:coverage": "vitest run --coverage",
29
- "lint": "oxlint src/",
30
- "format": "oxfmt --write src/",
31
- "format:check": "oxfmt --check src/",
32
- "typecheck": "tsc --noEmit",
33
- "check": "pnpm lint && pnpm format:check && pnpm typecheck"
34
- },
35
24
  "simple-git-hooks": {
36
25
  "pre-push": "pnpm check"
37
26
  },
@@ -60,5 +49,16 @@
60
49
  "tsdown": "^0.20.3",
61
50
  "typescript": "^5.9.3",
62
51
  "vitest": "^4.0.18"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage",
58
+ "lint": "oxlint src/",
59
+ "format": "oxfmt --write src/",
60
+ "format:check": "oxfmt --check src/",
61
+ "typecheck": "tsc --noEmit",
62
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck"
63
63
  }
64
- }
64
+ }