@buildinams/core 0.3.1 → 0.4.0

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.
Files changed (79) hide show
  1. package/README.md +24 -1
  2. package/dist/analytics/google/index.d.ts +158 -0
  3. package/dist/analytics/google/index.js +274 -0
  4. package/dist/analytics/google/index.js.map +1 -0
  5. package/dist/analytics/index.d.ts +101 -0
  6. package/dist/analytics/index.js +384 -0
  7. package/dist/analytics/index.js.map +1 -0
  8. package/dist/analytics/shopify/index.d.ts +186 -0
  9. package/dist/analytics/shopify/index.js +175 -0
  10. package/dist/analytics/shopify/index.js.map +1 -0
  11. package/dist/{chunk-5KDGGGVO.js → chunk-FJ7SNQQI.js} +24 -6
  12. package/dist/chunk-FJ7SNQQI.js.map +1 -0
  13. package/dist/chunk-G2BKC4BU.js +8 -0
  14. package/dist/chunk-G2BKC4BU.js.map +1 -0
  15. package/dist/chunk-GQVN2ECX.js +14 -0
  16. package/dist/chunk-GQVN2ECX.js.map +1 -0
  17. package/dist/chunk-LL3VTFNY.js +22 -0
  18. package/dist/chunk-LL3VTFNY.js.map +1 -0
  19. package/dist/chunk-M3HNM5HC.js +48 -0
  20. package/dist/chunk-M3HNM5HC.js.map +1 -0
  21. package/dist/chunk-MHFGMH3G.js +11 -0
  22. package/dist/chunk-MHFGMH3G.js.map +1 -0
  23. package/dist/chunk-OS2T6P2T.js +50 -0
  24. package/dist/chunk-OS2T6P2T.js.map +1 -0
  25. package/dist/chunk-QIJGG2F4.js +51 -0
  26. package/dist/chunk-QIJGG2F4.js.map +1 -0
  27. package/dist/consent/cookiebot/index.d.ts +16 -0
  28. package/dist/consent/cookiebot/index.js +27 -0
  29. package/dist/consent/cookiebot/index.js.map +1 -0
  30. package/dist/consent/google/index.d.ts +23 -0
  31. package/dist/consent/google/index.js +48 -0
  32. package/dist/consent/google/index.js.map +1 -0
  33. package/dist/consent/index.d.ts +1 -0
  34. package/dist/consent/index.js +3 -0
  35. package/dist/consent/index.js.map +1 -0
  36. package/dist/consent/onetrust/index.d.ts +23 -0
  37. package/dist/consent/onetrust/index.js +39 -0
  38. package/dist/consent/onetrust/index.js.map +1 -0
  39. package/dist/consent/shopify/index.d.ts +44 -0
  40. package/dist/consent/shopify/index.js +84 -0
  41. package/dist/consent/shopify/index.js.map +1 -0
  42. package/dist/cookies/index.d.ts +230 -0
  43. package/dist/cookies/index.js +188 -0
  44. package/dist/cookies/index.js.map +1 -0
  45. package/dist/define-events-CxZjoUlG.d.ts +255 -0
  46. package/dist/destination-5KJ5PeR3.d.ts +122 -0
  47. package/dist/index-Zef1nnTP.d.ts +36 -0
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +64 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/listeners-DkU-vBPa.d.ts +15 -0
  52. package/dist/navigation/index.js +1 -1
  53. package/dist/schema/index.d.ts +2 -0
  54. package/dist/schema/index.js +1 -1
  55. package/dist/store/index.js +1 -1
  56. package/dist/tracking/cart/index.d.ts +74 -0
  57. package/dist/tracking/cart/index.js +118 -0
  58. package/dist/tracking/cart/index.js.map +1 -0
  59. package/dist/tracking/dom/index.d.ts +320 -0
  60. package/dist/tracking/dom/index.js +1196 -0
  61. package/dist/tracking/dom/index.js.map +1 -0
  62. package/docs/analytics.md +883 -0
  63. package/docs/cookies.md +354 -0
  64. package/docs/decisions/001-analytics-project-events.md +31 -0
  65. package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
  66. package/docs/decisions/003-tracking-element-data.md +27 -0
  67. package/docs/decisions/004-cookies-explicit-store.md +26 -0
  68. package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
  69. package/docs/decisions/006-cookies-server-only-types.md +19 -0
  70. package/docs/decisions/007-cookies-options-on-definition.md +25 -0
  71. package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
  72. package/docs/decisions/009-analytics-consent-targets.md +29 -0
  73. package/docs/decisions/010-analytics-destination-contract.md +29 -0
  74. package/docs/decisions/011-tracking-decoupled.md +30 -0
  75. package/docs/tracking.md +221 -0
  76. package/package.json +83 -2
  77. package/dist/chunk-5KDGGGVO.js.map +0 -1
  78. package/dist/chunk-SIU7SKJK.js +0 -139
  79. package/dist/chunk-SIU7SKJK.js.map +0 -1
@@ -0,0 +1,1196 @@
1
+ import { createListeners } from '../../chunk-M3HNM5HC.js';
2
+ import { shape } from '../../chunk-G2BKC4BU.js';
3
+ import { shallowEqual } from '../../chunk-LL3VTFNY.js';
4
+ import { IS_DEVELOPMENT } from '../../chunk-GQVN2ECX.js';
5
+
6
+ // src/tracking/dom/roles.ts
7
+ var ROLE = "data-track";
8
+ var roleSelector = (role) => `[${ROLE}~="${role}"]`;
9
+ var hasRole = (element, role) => !!element.getAttribute(ROLE)?.split(/\s+/).includes(role);
10
+
11
+ // src/tracking/dom/attributes.ts
12
+ var toKebabCase = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
13
+ var toCamelCase = (name) => name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
14
+ var toName = (field) => `${ROLE}-${toKebabCase(field)}`;
15
+ var toField = (name) => toCamelCase(name.slice(ROLE.length + 1));
16
+ var getField = (element, field) => element.getAttribute(toName(field)) || void 0;
17
+ var BUILT_IN_FIELDS = {
18
+ "page-view": ["url", "title", "pageType"],
19
+ click: ["id"],
20
+ location: ["name"],
21
+ video: ["title"],
22
+ form: ["id"],
23
+ scroll: ["id"]
24
+ };
25
+ var isBuiltIn = (type, field) => !!BUILT_IN_FIELDS[type]?.includes(field);
26
+ var getCustomFields = (element, type) => {
27
+ const prefix = `${ROLE}-${type}-`;
28
+ const fields = {};
29
+ for (const { name, value } of element.attributes) {
30
+ if (name.startsWith(prefix) && value) {
31
+ fields[toField(`${ROLE}-${name.slice(prefix.length)}`)] = value;
32
+ }
33
+ }
34
+ return fields;
35
+ };
36
+ var toFieldAttributes = (type, data) => Object.fromEntries(
37
+ Object.entries(data).flatMap(
38
+ ([field, value]) => typeof value === "string" ? [[toName(isBuiltIn(type, field) ? field : `${type}-${field}`), value]] : []
39
+ )
40
+ );
41
+
42
+ // src/tracking/dom/auto-track.ts
43
+ var AUTO = [
44
+ roleSelector("auto-links"),
45
+ roleSelector("auto-forms"),
46
+ roleSelector("auto-none")
47
+ ].join(", ");
48
+ var isAutoTracked = (element, kind) => !!element.closest(AUTO)?.matches(roleSelector(`auto-${kind}`));
49
+
50
+ // src/tracking/dom/plugin.ts
51
+ var PLUGIN_ORDER = [
52
+ "pageViews",
53
+ "ecommerce",
54
+ "clicks",
55
+ "videos",
56
+ "forms",
57
+ "scroll"
58
+ ];
59
+ var definePlugin = (definition) => definition;
60
+ var toDefinition = (domPlugin) => domPlugin;
61
+
62
+ // src/tracking/dom/shared.ts
63
+ var CLICK = roleSelector("click");
64
+ var INTERACTIVE = `a[href], button, [role="button"], ${CLICK}`;
65
+ var MAX_TEXT_LENGTH = 100;
66
+ var MAX_RAW_TEXT_LENGTH = MAX_TEXT_LENGTH * 10;
67
+ var interactions = /* @__PURE__ */ new WeakMap();
68
+ var getInteraction = (event) => {
69
+ if (interactions.has(event)) return interactions.get(event);
70
+ const interaction = (
71
+ // Middle clicks only arrive as `auxclick`.
72
+ (event.type !== "auxclick" || event.button === 1) && event.target instanceof Element ? event.target.closest(INTERACTIVE) : null
73
+ );
74
+ interactions.set(event, interaction);
75
+ return interaction;
76
+ };
77
+ var getPage = () => window.location.pathname + window.location.search;
78
+ var isRendered = (element) => {
79
+ if (typeof element.checkVisibility === "function") {
80
+ return element.checkVisibility();
81
+ }
82
+ for (let node = element; node; node = node.parentElement) {
83
+ if (getComputedStyle(node).display === "none") return false;
84
+ }
85
+ return true;
86
+ };
87
+ var getLabelledBy = (element) => {
88
+ const ids = element.getAttribute("aria-labelledby")?.split(/\s+/);
89
+ if (!ids) return null;
90
+ return ids.map((id) => element.ownerDocument.getElementById(id)?.textContent).join(" ");
91
+ };
92
+ var collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
93
+ var getText = (element) => {
94
+ const raw = element.getAttribute("aria-label") ?? getLabelledBy(element) ?? element.textContent ?? "";
95
+ let text = collapseWhitespace(raw.slice(0, MAX_RAW_TEXT_LENGTH));
96
+ if (text.length < MAX_TEXT_LENGTH && raw.length > MAX_RAW_TEXT_LENGTH) {
97
+ text = collapseWhitespace(raw);
98
+ }
99
+ return text.slice(0, MAX_TEXT_LENGTH) || void 0;
100
+ };
101
+
102
+ // src/tracking/dom/clicks.ts
103
+ var LOCATION = roleSelector("location");
104
+ var getLocation = (element) => {
105
+ const area = element.closest(LOCATION);
106
+ if (!area) return void 0;
107
+ return getField(area, "name") || (area.parentElement ? getLocation(area.parentElement) : void 0);
108
+ };
109
+ var clicks = () => definePlugin({
110
+ name: "clicks",
111
+ events: { clicked: "A tracked link or button was clicked." },
112
+ install: ({ emit }) => {
113
+ const onClick = (event) => {
114
+ const element = getInteraction(event);
115
+ if (!element) return;
116
+ const isLink = element instanceof HTMLAnchorElement;
117
+ if (!element.matches(CLICK) && !(isLink && isAutoTracked(element, "links"))) {
118
+ return;
119
+ }
120
+ emit(
121
+ "clicked",
122
+ {
123
+ ...getCustomFields(element, "click"),
124
+ elementType: isLink ? "link" : "button",
125
+ elementId: getField(element, "id") || element.id || void 0,
126
+ text: getText(element),
127
+ url: isLink ? element.href : void 0,
128
+ outbound: isLink && element.protocol.startsWith("http") ? element.origin !== window.location.origin : void 0,
129
+ location: getLocation(element)
130
+ },
131
+ element
132
+ );
133
+ };
134
+ return {
135
+ listeners: [
136
+ ["click", onClick],
137
+ ["auxclick", onClick]
138
+ ]
139
+ };
140
+ }
141
+ });
142
+
143
+ // src/tracking/dom/attach.ts
144
+ var TRACK_ROLES = /* @__PURE__ */ new Set([
145
+ "view-item",
146
+ "quick-view",
147
+ "list",
148
+ "item",
149
+ "promotion"
150
+ ]);
151
+ var DATA = Symbol.for("@buildinams/core/tracking/dom");
152
+ var listeners = /* @__PURE__ */ new Set();
153
+ var onTrackData = (listener) => {
154
+ listeners.add(listener);
155
+ return () => {
156
+ listeners.delete(listener);
157
+ };
158
+ };
159
+ var getStored = (element, role) => hasRole(element, role) ? element[DATA]?.get(role) : void 0;
160
+ var readStored = (element, role) => {
161
+ const stored = getStored(element, role);
162
+ if (stored) stored.read = true;
163
+ return stored;
164
+ };
165
+ var resolve = (stored) => {
166
+ stored.resolved ??= {
167
+ value: typeof stored.value === "function" ? stored.value() : stored.value
168
+ };
169
+ return stored.resolved.value;
170
+ };
171
+ var getKey = (stored) => stored.key ??= JSON.stringify(resolve(stored));
172
+ var isLazy = (stored) => typeof stored.value === "function";
173
+ var isSame = (current, next) => {
174
+ if (isLazy(current) && !current.read) return false;
175
+ if (!isLazy(current) && !isLazy(next) && shallowEqual(current.value, next.value)) {
176
+ return true;
177
+ }
178
+ try {
179
+ return getKey(current) === getKey(next);
180
+ } catch {
181
+ return false;
182
+ }
183
+ };
184
+ var setTrackData = (element, role, value, toId) => {
185
+ const holder = element;
186
+ const data = holder[DATA] ??= /* @__PURE__ */ new Map();
187
+ const current = data.get(role);
188
+ if (value === void 0) {
189
+ if (!current) return;
190
+ data.delete(role);
191
+ } else if (current?.value === value) {
192
+ return;
193
+ } else {
194
+ const next = { value, toId };
195
+ if (current && !current.read && isLazy(next)) {
196
+ data.set(role, next);
197
+ return;
198
+ }
199
+ if (current && isSame(current, next)) return;
200
+ data.set(role, next);
201
+ }
202
+ listeners.forEach((listener) => listener(element, role));
203
+ };
204
+ var getTrackData = (element, role) => {
205
+ const stored = readStored(element, role);
206
+ return stored && resolve(stored);
207
+ };
208
+ var hasTrackData = (element, role) => !!getStored(element, role);
209
+ var getTrackKey = (element, role) => {
210
+ const stored = readStored(element, role);
211
+ return stored ? getKey(stored) : "";
212
+ };
213
+ var getTrackId = (element, role) => {
214
+ const stored = readStored(element, role);
215
+ if (!stored) return "";
216
+ if (stored.id === void 0) {
217
+ const toId = stored.toId;
218
+ try {
219
+ stored.id = toId ? toId(resolve(stored)) : getTrackKey(element, role);
220
+ } catch (error) {
221
+ console.error("[analytics] ecommerce id", error);
222
+ stored.id = getTrackKey(element, role);
223
+ }
224
+ }
225
+ return stored.id;
226
+ };
227
+
228
+ // src/tracking/dom/track.ts
229
+ var getTypes = (element) => element.getAttribute(ROLE)?.split(/\s+/).filter(Boolean) ?? [];
230
+ var setTypes = (element, types) => {
231
+ if (types.length > 0) element.setAttribute(ROLE, types.join(" "));
232
+ else element.removeAttribute(ROLE);
233
+ };
234
+ var toTrackProps = (spec, idFor) => {
235
+ const types = [];
236
+ const fields = {};
237
+ const data = [];
238
+ for (const [key, value] of Object.entries(spec)) {
239
+ const type = toKebabCase(key);
240
+ if (TRACK_ROLES.has(type)) {
241
+ data.push([type, value]);
242
+ if (value !== void 0) types.push(type);
243
+ } else if (type === "auto") {
244
+ if (value === false) types.push("auto-none");
245
+ else if (value !== void 0) {
246
+ for (const kind of [value].flat()) types.push(`auto-${kind}`);
247
+ }
248
+ } else if (value !== void 0 && value !== false) {
249
+ types.push(type);
250
+ Object.assign(
251
+ fields,
252
+ toFieldAttributes(type, value === true ? {} : value)
253
+ );
254
+ }
255
+ }
256
+ const props = {
257
+ ...fields,
258
+ ...types.length > 0 && { [ROLE]: types.join(" ") }
259
+ };
260
+ if (data.length === 0) return props;
261
+ return {
262
+ ...props,
263
+ // Also adds or removes the types, for an element rendered without spreading the props.
264
+ ref: (element) => {
265
+ if (!element) return;
266
+ for (const [role, value] of data) {
267
+ const current = getTypes(element);
268
+ if (value === void 0) {
269
+ if (current.includes(role)) {
270
+ setTypes(
271
+ element,
272
+ current.filter((type) => type !== role)
273
+ );
274
+ }
275
+ setTrackData(element, role, void 0);
276
+ } else {
277
+ setTrackData(element, role, value, idFor(role));
278
+ if (!current.includes(role)) setTypes(element, [...current, role]);
279
+ }
280
+ }
281
+ }
282
+ };
283
+ };
284
+
285
+ // src/tracking/dom/create-dom-watcher.ts
286
+ var elementsIn = (node, selector) => node instanceof Element ? [
287
+ ...node.matches(selector) ? [node] : [],
288
+ ...node.querySelectorAll(selector)
289
+ ] : [];
290
+ var roots = /* @__PURE__ */ new WeakMap();
291
+ var notify = (state, mutations) => {
292
+ if (mutations.length === 0) return;
293
+ const found = mutations.map((mutation) => {
294
+ if (mutation.type !== "childList" || !state.selector) return void 0;
295
+ const added = [...mutation.addedNodes].flatMap(
296
+ (node) => elementsIn(node, state.selector)
297
+ );
298
+ const removed = [...mutation.removedNodes].flatMap(
299
+ (node) => elementsIn(node, state.selector)
300
+ );
301
+ return added.length > 0 || removed.length > 0 ? { added, removed } : void 0;
302
+ });
303
+ for (const instance of state.ordered) {
304
+ if (!instance.changed) continue;
305
+ const { selector, attributes = [] } = instance;
306
+ const changes = mutations.flatMap((mutation, index) => {
307
+ if (mutation.type === "attributes") {
308
+ return attributes.includes(mutation.attributeName) ? [
309
+ {
310
+ type: "attribute",
311
+ element: mutation.target,
312
+ attribute: mutation.attributeName
313
+ }
314
+ ] : [];
315
+ }
316
+ const children = found[index];
317
+ if (!selector || !children) return [];
318
+ const matches = (element) => element.matches(selector);
319
+ const added = children.added.filter(matches);
320
+ const removed = children.removed.filter(matches);
321
+ return added.length > 0 || removed.length > 0 ? [{ type: "children", added, removed }] : [];
322
+ });
323
+ if (changes.length > 0) instance.changed(changes);
324
+ }
325
+ };
326
+ var notifyData = (state) => {
327
+ if (state.data.size === 0) return;
328
+ const changed = [...state.data];
329
+ state.data.clear();
330
+ for (const instance of state.ordered) {
331
+ const { data: roles = [] } = instance;
332
+ if (!instance.changed || roles.length === 0) continue;
333
+ const changes = changed.flatMap(
334
+ ([element, elementRoles]) => !element.isConnected ? [] : [...elementRoles].filter((role) => roles.includes(role)).map((role) => ({ type: "data", element, role }))
335
+ );
336
+ if (changes.length > 0) instance.changed(changes);
337
+ }
338
+ };
339
+ var release = (state) => {
340
+ const held = state.held;
341
+ state.held = [];
342
+ held.forEach((deliver) => deliver());
343
+ };
344
+ var takeChanges = (state) => {
345
+ notify(state, state.observer.takeRecords());
346
+ notifyData(state);
347
+ };
348
+ var createState = (root) => {
349
+ const state = {
350
+ instances: /* @__PURE__ */ new Map(),
351
+ ordered: [],
352
+ selector: "",
353
+ observer: new MutationObserver((mutations) => notify(state, mutations)),
354
+ handlers: /* @__PURE__ */ new Map(),
355
+ dispatchers: /* @__PURE__ */ new Map(),
356
+ data: /* @__PURE__ */ new Map(),
357
+ held: [],
358
+ // After the task's other data, as React sets children's refs before their parents': an item handled straight away
359
+ // would miss its list's data.
360
+ stopData: onTrackData((element, role) => {
361
+ if (!root.contains(element)) return;
362
+ if (state.data.size === 0) queueMicrotask(() => takeChanges(state));
363
+ const roles = state.data.get(element) ?? /* @__PURE__ */ new Set();
364
+ state.data.set(element, roles.add(role));
365
+ })
366
+ };
367
+ roots.set(root, state);
368
+ return state;
369
+ };
370
+ var configure = (root, state) => {
371
+ state.ordered = PLUGIN_ORDER.flatMap((name) => {
372
+ const instance = state.instances.get(name);
373
+ return instance ? [instance] : [];
374
+ });
375
+ state.selector = state.ordered.flatMap(({ selector }) => selector ? [selector] : []).join(", ");
376
+ const attributes = [
377
+ ...new Set(state.ordered.flatMap(({ attributes: attributes2 = [] }) => attributes2))
378
+ ];
379
+ state.observer.disconnect();
380
+ if (state.selector || attributes.length > 0) {
381
+ state.observer.observe(root, {
382
+ childList: !!state.selector,
383
+ subtree: true,
384
+ ...attributes.length > 0 && {
385
+ attributes: true,
386
+ attributeFilter: attributes
387
+ }
388
+ });
389
+ }
390
+ state.handlers = /* @__PURE__ */ new Map();
391
+ for (const { listeners: listeners2 = [] } of state.ordered) {
392
+ for (const [type, listener] of listeners2) {
393
+ state.handlers.set(type, [...state.handlers.get(type) ?? [], listener]);
394
+ }
395
+ }
396
+ for (const [type, dispatcher] of state.dispatchers) {
397
+ if (state.handlers.has(type)) continue;
398
+ root.removeEventListener(type, dispatcher, true);
399
+ state.dispatchers.delete(type);
400
+ }
401
+ for (const type of state.handlers.keys()) {
402
+ if (state.dispatchers.has(type)) continue;
403
+ const dispatcher = (event) => state.handlers.get(type)?.forEach((handler) => handler(event));
404
+ root.addEventListener(type, dispatcher, true);
405
+ state.dispatchers.set(type, dispatcher);
406
+ }
407
+ if (state.instances.size === 0) {
408
+ state.stopData();
409
+ roots.delete(root);
410
+ }
411
+ };
412
+ var remove = (state, name) => {
413
+ const instance = state.instances.get(name);
414
+ if (!instance) return;
415
+ state.instances.delete(name);
416
+ instance.stop?.();
417
+ };
418
+ var createDomWatcher = (plugins, { root: rootOption } = {}) => {
419
+ const definitions = plugins.map(toDefinition);
420
+ const toId = [...definitions].reverse().find(({ name }) => name === "ecommerce")?.toId;
421
+ const idFor = (type) => type === "list" || type === "promotion" ? void 0 : toId;
422
+ let running;
423
+ const isCurrent = ({ state, installed }) => [...installed].some(
424
+ ([name, instance]) => state.instances.get(name) === instance
425
+ );
426
+ const eachInstance = (method) => {
427
+ if (!running || !isCurrent(running)) return;
428
+ const { state } = running;
429
+ takeChanges(state);
430
+ state.ordered.forEach((instance) => instance[method]?.());
431
+ };
432
+ let isStarting = false;
433
+ const start = () => {
434
+ const root = rootOption ?? document;
435
+ const state = roots.get(root) ?? createState(root);
436
+ takeChanges(state);
437
+ isStarting = true;
438
+ const context = {
439
+ emit: (name, payload, element) => {
440
+ for (const instance of state.ordered) {
441
+ try {
442
+ instance.beforeEmit?.();
443
+ } catch (error) {
444
+ console.error("[analytics] beforeEmit", error);
445
+ }
446
+ }
447
+ const event = element ? { name, payload, element } : { name, payload };
448
+ if (isStarting) {
449
+ state.held.push(() => listeners2.emit(event));
450
+ return;
451
+ }
452
+ release(state);
453
+ listeners2.emit(event);
454
+ },
455
+ root,
456
+ takeChanges: () => takeChanges(state),
457
+ newPage: () => state.ordered.forEach((instance) => instance.newPage?.())
458
+ };
459
+ const installed = /* @__PURE__ */ new Map();
460
+ for (const { name, install } of definitions) {
461
+ remove(state, name);
462
+ const instance = install(context);
463
+ state.instances.set(name, instance);
464
+ installed.set(name, instance);
465
+ }
466
+ configure(root, state);
467
+ const rootElement = root instanceof Document ? root.documentElement : root;
468
+ for (const name of PLUGIN_ORDER) {
469
+ const instance = installed.get(name);
470
+ if (!instance?.selector || !instance.changed) continue;
471
+ instance.changed([
472
+ {
473
+ type: "children",
474
+ added: elementsIn(rootElement, instance.selector),
475
+ removed: []
476
+ }
477
+ ]);
478
+ }
479
+ isStarting = false;
480
+ running = { root, state, installed };
481
+ queueMicrotask(() => release(state));
482
+ };
483
+ const listeners2 = createListeners({
484
+ start: () => {
485
+ try {
486
+ start();
487
+ } finally {
488
+ isStarting = false;
489
+ }
490
+ },
491
+ stop: () => {
492
+ if (!running) return;
493
+ const { root, state, installed } = running;
494
+ running = void 0;
495
+ takeChanges(state);
496
+ release(state);
497
+ for (const [name, instance] of installed) {
498
+ if (state.instances.get(name) === instance) remove(state, name);
499
+ }
500
+ configure(root, state);
501
+ }
502
+ });
503
+ return {
504
+ track: ((spec) => toTrackProps(spec, idFor)),
505
+ subscribe: listeners2.subscribe,
506
+ events: Object.fromEntries(
507
+ definitions.flatMap(
508
+ ({ events }) => Object.entries(events).map(([name, description]) => [
509
+ name,
510
+ { description, payload: shape() }
511
+ ])
512
+ )
513
+ ),
514
+ newPage: () => eachInstance("newPage"),
515
+ flush: () => eachInstance("flush")
516
+ };
517
+ };
518
+
519
+ // src/tracking/dom/ecommerce.ts
520
+ var PROMOTION = roleSelector("promotion");
521
+ var QUICK_VIEW = roleSelector("quick-view");
522
+ var VIEW_ITEM = `${roleSelector("view-item")}, ${QUICK_VIEW}`;
523
+ var ITEM_LIST = roleSelector("list");
524
+ var ITEM = roleSelector("item");
525
+ var ROLES = ["promotion", "view-item", "quick-view", "item", "list"];
526
+ var MAX_BATCH_SIZE = 30;
527
+ var MISSING_DATA_DELAY = 2e3;
528
+ var URL_CHANGE_DELAY = 100;
529
+ var OBSERVED = `${PROMOTION}, ${ITEM}, ${VIEW_ITEM}`;
530
+ var LINE_HEIGHT = 1e-3;
531
+ var MAX_STORED_PLACEMENTS = 50;
532
+ var getListElement = (itemElement) => itemElement.parentElement?.closest(ITEM_LIST) ?? null;
533
+ var getFeaturedIn = (itemElement, listElement) => {
534
+ const promotionElement = itemElement.closest(PROMOTION);
535
+ return promotionElement && !(listElement && promotionElement.contains(listElement)) ? promotionElement : null;
536
+ };
537
+ var definedFields = (fields) => Object.fromEntries(
538
+ Object.entries(fields).filter(([, value]) => value !== void 0)
539
+ );
540
+ var toLines = (threshold) => {
541
+ const lines = threshold <= 0.5 ? [[threshold, threshold]] : [
542
+ [1 - threshold, threshold],
543
+ [threshold, 1 - threshold]
544
+ ];
545
+ return lines.map(
546
+ ([top, bottom]) => top + bottom >= 1 ? [top - LINE_HEIGHT / 2, bottom - LINE_HEIGHT / 2] : [top, bottom]
547
+ );
548
+ };
549
+ var toMargin = (share) => `${-Math.round(share * 1e4) / 100}%`;
550
+ var isItem = (element) => element.matches(ITEM);
551
+ var isPlacement = (entry) => {
552
+ if (!Array.isArray(entry) || typeof entry[0] !== "string") return false;
553
+ const placement = entry[1];
554
+ if (typeof placement !== "object" || placement === null || Array.isArray(placement)) {
555
+ return false;
556
+ }
557
+ const { index } = placement;
558
+ return index === void 0 || typeof index === "number";
559
+ };
560
+ var readPlacements = (key) => {
561
+ try {
562
+ const value = sessionStorage.getItem(key);
563
+ const entries = value ? JSON.parse(value) : [];
564
+ return new Map(
565
+ Array.isArray(entries) ? entries.filter(isPlacement).slice(-MAX_STORED_PLACEMENTS) : []
566
+ );
567
+ } catch {
568
+ return /* @__PURE__ */ new Map();
569
+ }
570
+ };
571
+ var writePlacements = (key, placements) => {
572
+ try {
573
+ sessionStorage.setItem(key, JSON.stringify([...placements]));
574
+ } catch {
575
+ }
576
+ };
577
+ var ecommerce = ({
578
+ id,
579
+ threshold = 0.5,
580
+ batchDelay = 500,
581
+ storageKey = "analytics-placements"
582
+ } = {}) => definePlugin({
583
+ name: "ecommerce",
584
+ events: {
585
+ itemViewed: "A product was viewed on its page or in a quick view, placed where it was last selected.",
586
+ itemSelected: "A product in a list was clicked.",
587
+ itemsViewed: "Products in a list came into view, batched per list.",
588
+ promotionViewed: "A promotion came into view.",
589
+ promotionSelected: "A promotion was clicked."
590
+ },
591
+ toId: id,
592
+ install: (context) => {
593
+ const { takeChanges: takeChanges2 } = context;
594
+ let isTrackingViews = false;
595
+ const emit = (key, occurrence) => {
596
+ const wasTracking = isTrackingViews;
597
+ isTrackingViews = key === "itemsViewed" || key === "promotionViewed";
598
+ try {
599
+ context.emit(key, occurrence);
600
+ } finally {
601
+ isTrackingViews = wasTracking;
602
+ }
603
+ };
604
+ const checked = /* @__PURE__ */ new WeakSet();
605
+ const warnWithoutData = (element, type) => {
606
+ if (!IS_DEVELOPMENT || checked.has(element)) return;
607
+ checked.add(element);
608
+ setTimeout(() => {
609
+ if (element.isConnected && hasRole(element, type) && !hasTrackData(element, type)) {
610
+ console.warn(
611
+ `[analytics] data-track="${type}" has no data. Set the ref from track({ ${toCamelCase(type)} }) on it.`,
612
+ element
613
+ );
614
+ }
615
+ }, MISSING_DATA_DELAY);
616
+ };
617
+ const viewed = /* @__PURE__ */ new Set();
618
+ let viewedPage;
619
+ const visible = /* @__PURE__ */ new Set();
620
+ const pending = /* @__PURE__ */ new Map();
621
+ let timeout;
622
+ const flushViews = () => {
623
+ clearTimeout(timeout);
624
+ timeout = void 0;
625
+ const views = [...pending.values()];
626
+ pending.clear();
627
+ for (const view2 of views) {
628
+ if ("promotion" in view2) {
629
+ emit("promotionViewed", view2);
630
+ } else {
631
+ emit("itemsViewed", definedFields(view2));
632
+ }
633
+ }
634
+ };
635
+ const scheduleFlush = () => {
636
+ timeout ??= setTimeout(flushViews, batchDelay);
637
+ };
638
+ const viewedOnPage = () => {
639
+ if (viewedPage !== getPage()) {
640
+ viewed.clear();
641
+ viewedPage = getPage();
642
+ }
643
+ return viewed;
644
+ };
645
+ const isNewView = (key) => {
646
+ if (viewedOnPage().has(key)) return false;
647
+ viewed.add(key);
648
+ return true;
649
+ };
650
+ const selectedFrom = readPlacements(storageKey);
651
+ const setSelectedFrom = (product, placement) => {
652
+ selectedFrom.delete(product);
653
+ selectedFrom.set(product, placement);
654
+ for (const oldest of selectedFrom.keys()) {
655
+ if (selectedFrom.size <= MAX_STORED_PLACEMENTS) break;
656
+ selectedFrom.delete(oldest);
657
+ }
658
+ writePlacements(storageKey, selectedFrom);
659
+ };
660
+ let indexes = /* @__PURE__ */ new WeakMap();
661
+ const getIndex = (listElement, itemElement) => {
662
+ let positions = indexes.get(listElement);
663
+ if (!positions) {
664
+ positions = /* @__PURE__ */ new Map();
665
+ for (const other of listElement.querySelectorAll(ITEM)) {
666
+ if (getListElement(other) === listElement) {
667
+ positions.set(other, positions.size);
668
+ }
669
+ }
670
+ indexes.set(listElement, positions);
671
+ }
672
+ return positions.get(itemElement) ?? -1;
673
+ };
674
+ const toItem = (element, listElement = getListElement(element)) => {
675
+ const item = getTrackData(element, "item");
676
+ if (!item) {
677
+ warnWithoutData(element, "item");
678
+ return void 0;
679
+ }
680
+ const list = listElement && getTrackData(listElement, "list") || void 0;
681
+ if (listElement && !list) warnWithoutData(listElement, "list");
682
+ const promotionElement = element.closest(PROMOTION);
683
+ const promotion = promotionElement && getTrackData(promotionElement, "promotion") || void 0;
684
+ return {
685
+ list,
686
+ featuredIn: getFeaturedIn(element, listElement),
687
+ placed: definedFields({
688
+ item,
689
+ index: listElement ? getIndex(listElement, element) : void 0,
690
+ promotion
691
+ })
692
+ };
693
+ };
694
+ const shownItems = /* @__PURE__ */ new WeakMap();
695
+ const trackViewItem = (element) => {
696
+ const isQuickView = element.matches(QUICK_VIEW);
697
+ const type = isQuickView ? "quick-view" : "view-item";
698
+ const item = getTrackData(element, type);
699
+ if (!item) {
700
+ warnWithoutData(element, type);
701
+ return;
702
+ }
703
+ const product = getTrackId(element, type);
704
+ if (isQuickView) {
705
+ const key = `${getPage()}${product}`;
706
+ if (shownItems.get(element) === key) return;
707
+ shownItems.set(element, key);
708
+ } else if (!isNewView(`${VIEW_ITEM}${product}`)) {
709
+ return;
710
+ }
711
+ emit("itemViewed", { ...selectedFrom.get(product), item });
712
+ };
713
+ const viewItem = (element) => {
714
+ const listElement = getListElement(element);
715
+ if (getFeaturedIn(element, listElement)) return;
716
+ const key = `${ITEM}${listElement ? getTrackKey(listElement, "list") : ""}${getTrackId(element, "item")}`;
717
+ if (viewedOnPage().has(key)) return;
718
+ const resolved = toItem(element, listElement);
719
+ if (!resolved) return;
720
+ viewed.add(key);
721
+ const { list, placed } = resolved;
722
+ const batch = pending.get(listElement);
723
+ if (batch && !("promotion" in batch)) {
724
+ batch.items.push(placed);
725
+ if (batch.items.length >= MAX_BATCH_SIZE) flushViews();
726
+ } else {
727
+ pending.set(listElement, { list, items: [placed] });
728
+ }
729
+ scheduleFlush();
730
+ };
731
+ const view = (element) => {
732
+ if (!element.isConnected) return;
733
+ if (element.matches(VIEW_ITEM)) trackViewItem(element);
734
+ if (element.matches(ITEM)) viewItem(element);
735
+ const key = `${PROMOTION}${getTrackKey(element, "promotion")}`;
736
+ if (!element.matches(PROMOTION) || viewedOnPage().has(key)) return;
737
+ const promotion = getTrackData(element, "promotion");
738
+ if (!promotion) {
739
+ warnWithoutData(element, "promotion");
740
+ return;
741
+ }
742
+ viewed.add(key);
743
+ const items = [...element.querySelectorAll(ITEM)].flatMap(
744
+ (itemElement) => {
745
+ const listElement = getListElement(itemElement);
746
+ if (getFeaturedIn(itemElement, listElement) !== element) return [];
747
+ if (listElement && !hasTrackData(listElement, "list")) {
748
+ warnWithoutData(listElement, "list");
749
+ }
750
+ const item = getTrackData(itemElement, "item");
751
+ if (!item) warnWithoutData(itemElement, "item");
752
+ return item ? [item] : [];
753
+ }
754
+ );
755
+ const promotionView = { promotion, items };
756
+ pending.set(promotionView, promotionView);
757
+ scheduleFlush();
758
+ };
759
+ const onClick = (event) => {
760
+ const element = getInteraction(event);
761
+ if (!element) return;
762
+ takeChanges2();
763
+ const promotionElement = element.closest(PROMOTION);
764
+ const promotion = promotionElement && getTrackData(promotionElement, "promotion");
765
+ const itemElement = element.closest(ITEM);
766
+ const resolved = itemElement && toItem(itemElement);
767
+ const isFeatured = !!resolved?.featuredIn;
768
+ const isInPromotedList = !!resolved && !isFeatured && !!promotionElement?.contains(itemElement);
769
+ if (promotion) view(promotionElement);
770
+ if (resolved) view(itemElement);
771
+ if (promotion && !isInPromotedList) {
772
+ emit(
773
+ "promotionSelected",
774
+ definedFields({
775
+ promotion,
776
+ item: isFeatured ? resolved.placed.item : void 0
777
+ })
778
+ );
779
+ }
780
+ if (!resolved) return;
781
+ const { list, placed } = resolved;
782
+ setSelectedFrom(
783
+ getTrackId(itemElement, "item"),
784
+ definedFields({
785
+ list,
786
+ index: placed.index,
787
+ promotion: placed.promotion
788
+ })
789
+ );
790
+ if (!isFeatured) {
791
+ emit("itemSelected", definedFields({ list, ...placed }));
792
+ }
793
+ };
794
+ const setInView = (target, isInView) => {
795
+ if (isInView && visible.has(target)) return;
796
+ if (isInView && isRendered(target)) {
797
+ visible.add(target);
798
+ view(target);
799
+ } else {
800
+ visible.delete(target);
801
+ shownItems.delete(target);
802
+ }
803
+ };
804
+ const reports = /* @__PURE__ */ new WeakMap();
805
+ const report = (target, update) => {
806
+ const state = reports.get(target);
807
+ if (!state) return;
808
+ update(state);
809
+ if (state.ratio === void 0 || state.lines.includes(void 0))
810
+ return;
811
+ setInView(target, state.ratio || state.lines.every(Boolean));
812
+ };
813
+ const ratioObserver = new IntersectionObserver(
814
+ (entries) => {
815
+ for (const { target, isIntersecting, intersectionRatio } of entries) {
816
+ report(target, (state) => {
817
+ state.ratio = isIntersecting && intersectionRatio >= threshold;
818
+ });
819
+ }
820
+ },
821
+ { threshold }
822
+ );
823
+ const lineObservers = toLines(threshold).map(
824
+ ([top, bottom], index) => new IntersectionObserver(
825
+ (entries) => {
826
+ for (const { target, isIntersecting } of entries) {
827
+ report(target, (state) => {
828
+ state.lines[index] = isIntersecting;
829
+ });
830
+ }
831
+ },
832
+ {
833
+ // The document, not the implicit root, as that ignores the margin in a cross-origin iframe.
834
+ root: document,
835
+ rootMargin: `${toMargin(top)} 0px ${toMargin(bottom)} 0px`
836
+ }
837
+ )
838
+ );
839
+ const observe = (element) => {
840
+ reports.set(element, { lines: lineObservers.map(() => void 0) });
841
+ ratioObserver.observe(element);
842
+ lineObservers.forEach((observer) => observer.observe(element));
843
+ };
844
+ const unobserve = (element) => {
845
+ reports.delete(element);
846
+ ratioObserver.unobserve(element);
847
+ lineObservers.forEach((observer) => observer.unobserve(element));
848
+ };
849
+ let observedPath = window.location.pathname;
850
+ let hasPageViews = false;
851
+ const observeAnew = () => {
852
+ observedPath = window.location.pathname;
853
+ flushViews();
854
+ visible.forEach((element) => {
855
+ unobserve(element);
856
+ observe(element);
857
+ });
858
+ visible.clear();
859
+ };
860
+ const onHide = () => {
861
+ if (document.visibilityState === "hidden") flushViews();
862
+ };
863
+ window.addEventListener("pagehide", flushViews);
864
+ document.addEventListener("visibilitychange", onHide);
865
+ let urlChange;
866
+ const onUrlChange = () => {
867
+ clearTimeout(urlChange);
868
+ urlChange = setTimeout(() => {
869
+ visible.forEach((element) => {
870
+ if (element.isConnected) trackViewItem(element);
871
+ });
872
+ }, URL_CHANGE_DELAY);
873
+ };
874
+ const navigation = window.navigation;
875
+ navigation?.addEventListener("currententrychange", onUrlChange);
876
+ window.addEventListener("popstate", onUrlChange);
877
+ const dataChanged = (element) => {
878
+ const wasVisible = visible.has(element);
879
+ unobserve(element);
880
+ visible.delete(element);
881
+ if (!hasTrackData(element, "view-item") && !hasTrackData(element, "quick-view")) {
882
+ shownItems.delete(element);
883
+ }
884
+ if (!element.matches(OBSERVED)) return;
885
+ observe(element);
886
+ if (wasVisible) {
887
+ visible.add(element);
888
+ view(element);
889
+ }
890
+ };
891
+ return {
892
+ selector: OBSERVED,
893
+ attributes: [ROLE],
894
+ data: ROLES,
895
+ listeners: [
896
+ ["click", onClick],
897
+ ["auxclick", onClick]
898
+ ],
899
+ // So pending views come before any interaction or page view. Without page views, a new path means the router
900
+ // shows a page the project tracks itself.
901
+ beforeEmit: () => {
902
+ if (isTrackingViews) return;
903
+ if (hasPageViews || window.location.pathname === observedPath) {
904
+ flushViews();
905
+ } else {
906
+ observeAnew();
907
+ }
908
+ },
909
+ newPage: () => {
910
+ hasPageViews = true;
911
+ observeAnew();
912
+ },
913
+ flush: flushViews,
914
+ changed: (changes) => {
915
+ const movesItems = changes.some(
916
+ (change) => change.type === "children" ? change.added.some(isItem) || change.removed.some(isItem) : change.type === "attribute" || change.role === "item" || change.role === "list"
917
+ );
918
+ if (movesItems) indexes = /* @__PURE__ */ new WeakMap();
919
+ for (const change of changes) {
920
+ if (change.type === "children") {
921
+ change.removed.forEach((element) => {
922
+ unobserve(element);
923
+ visible.delete(element);
924
+ shownItems.delete(element);
925
+ });
926
+ change.added.forEach(observe);
927
+ continue;
928
+ }
929
+ dataChanged(change.element);
930
+ }
931
+ },
932
+ stop: () => {
933
+ flushViews();
934
+ window.removeEventListener("pagehide", flushViews);
935
+ document.removeEventListener("visibilitychange", onHide);
936
+ navigation?.removeEventListener("currententrychange", onUrlChange);
937
+ window.removeEventListener("popstate", onUrlChange);
938
+ clearTimeout(urlChange);
939
+ ratioObserver.disconnect();
940
+ lineObservers.forEach((observer) => observer.disconnect());
941
+ }
942
+ };
943
+ }
944
+ });
945
+
946
+ // src/tracking/dom/forms.ts
947
+ var FORM = roleSelector("form");
948
+ var isTracked = (form) => form.matches(FORM) || isAutoTracked(form, "forms");
949
+ var toForm = (form) => {
950
+ const action = form.getAttribute("action");
951
+ return {
952
+ ...getCustomFields(form, "form"),
953
+ formId: getField(form, "id") || form.getAttribute("id") || void 0,
954
+ formName: form.getAttribute("name") || void 0,
955
+ formDestination: action ? new URL(action, window.location.href).href : window.location.href
956
+ };
957
+ };
958
+ var forms = () => definePlugin({
959
+ name: "forms",
960
+ events: {
961
+ formStarted: "A tracked form was first interacted with on this page.",
962
+ formSubmitted: "A tracked form was submitted."
963
+ },
964
+ install: ({ emit }) => {
965
+ const seenForms = /* @__PURE__ */ new WeakMap();
966
+ const onFormInput = (event) => {
967
+ if (!(event.target instanceof Element)) return;
968
+ const form = event.target instanceof HTMLFormElement ? event.target : event.target.closest("form");
969
+ if (!form || seenForms.get(form) === getPage()) return;
970
+ seenForms.set(form, getPage());
971
+ if (isTracked(form)) {
972
+ emit("formStarted", toForm(form), form);
973
+ }
974
+ };
975
+ const onSubmit = (event) => {
976
+ const form = event.target;
977
+ if (!(form instanceof HTMLFormElement && isTracked(form))) {
978
+ return;
979
+ }
980
+ const { submitter } = event;
981
+ emit(
982
+ "formSubmitted",
983
+ {
984
+ ...toForm(form),
985
+ formSubmitText: submitter ? getText(submitter) : void 0
986
+ },
987
+ form
988
+ );
989
+ };
990
+ return {
991
+ listeners: [
992
+ ["input", onFormInput],
993
+ ["change", onFormInput],
994
+ ["submit", onSubmit]
995
+ ]
996
+ };
997
+ }
998
+ });
999
+
1000
+ // src/tracking/dom/page-views.ts
1001
+ var PAGE_VIEW = roleSelector("page-view");
1002
+ var PAGE_FIELDS = ["url", "title", "pageType"];
1003
+ var pageViews = () => definePlugin({
1004
+ name: "pageViews",
1005
+ events: {
1006
+ pageViewed: "A page was shown: on load, after a client-side navigation, and when a hidden page is shown again."
1007
+ },
1008
+ install: ({ emit, newPage }) => {
1009
+ let lastPageView;
1010
+ const trackPageView = (element) => {
1011
+ if (lastPageView === getPage() || !element.matches(PAGE_VIEW)) return;
1012
+ if (!isRendered(element)) return;
1013
+ lastPageView = getPage();
1014
+ newPage();
1015
+ emit(
1016
+ "pageViewed",
1017
+ {
1018
+ ...getCustomFields(element, "page-view"),
1019
+ ...Object.fromEntries(
1020
+ PAGE_FIELDS.flatMap((field) => {
1021
+ const value = getField(element, field);
1022
+ return value ? [[field, value]] : [];
1023
+ })
1024
+ )
1025
+ },
1026
+ element
1027
+ );
1028
+ };
1029
+ const resizes = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(
1030
+ (entries) => entries.forEach(({ target }) => trackPageView(target))
1031
+ );
1032
+ return {
1033
+ selector: PAGE_VIEW,
1034
+ attributes: [ROLE, `${ROLE}-page-type`, `${ROLE}-url`, `${ROLE}-title`],
1035
+ changed: (changes) => {
1036
+ for (const change of changes) {
1037
+ if (change.type === "children") {
1038
+ change.removed.forEach((element) => resizes?.unobserve(element));
1039
+ change.added.forEach((element) => {
1040
+ resizes?.observe(element);
1041
+ trackPageView(element);
1042
+ });
1043
+ } else if (change.type === "attribute") {
1044
+ if (change.element.matches(PAGE_VIEW)) {
1045
+ resizes?.observe(change.element);
1046
+ }
1047
+ trackPageView(change.element);
1048
+ }
1049
+ }
1050
+ },
1051
+ stop: () => resizes?.disconnect()
1052
+ };
1053
+ }
1054
+ });
1055
+
1056
+ // src/tracking/dom/scroll.ts
1057
+ var SCROLL = roleSelector("scroll");
1058
+ var getPercent = (element) => {
1059
+ const { top, height } = element.getBoundingClientRect();
1060
+ if (height === 0 || !isRendered(element)) return 0;
1061
+ const percent = (window.innerHeight - top) / height * 100;
1062
+ return Math.min(Math.max(percent, 0), 100);
1063
+ };
1064
+ var scroll = ({
1065
+ thresholds = [25, 50, 75, 90]
1066
+ } = {}) => definePlugin({
1067
+ name: "scroll",
1068
+ events: {
1069
+ scrolled: "A tracked element was scrolled through to a threshold, once per page."
1070
+ },
1071
+ install: ({ emit }) => {
1072
+ const ascending = [...thresholds].sort((a, b) => a - b);
1073
+ const elements = /* @__PURE__ */ new Set();
1074
+ const reached = /* @__PURE__ */ new WeakMap();
1075
+ let frame;
1076
+ const check = () => {
1077
+ frame = void 0;
1078
+ const page = getPage();
1079
+ for (const element of elements) {
1080
+ const percent = getPercent(element);
1081
+ let state = reached.get(element);
1082
+ if (state?.page !== page) {
1083
+ state = { page, percents: /* @__PURE__ */ new Set() };
1084
+ reached.set(element, state);
1085
+ }
1086
+ for (const threshold of ascending) {
1087
+ if (percent < threshold || state.percents.has(threshold)) continue;
1088
+ state.percents.add(threshold);
1089
+ emit(
1090
+ "scrolled",
1091
+ {
1092
+ ...getCustomFields(element, "scroll"),
1093
+ percentScrolled: threshold,
1094
+ elementId: getField(element, "id") || element.id || void 0
1095
+ },
1096
+ element
1097
+ );
1098
+ }
1099
+ }
1100
+ };
1101
+ const onScroll = () => {
1102
+ if (elements.size > 0) frame ??= requestAnimationFrame(check);
1103
+ };
1104
+ window.addEventListener("scroll", onScroll, {
1105
+ capture: true,
1106
+ passive: true
1107
+ });
1108
+ return {
1109
+ selector: SCROLL,
1110
+ attributes: [ROLE],
1111
+ changed: (changes) => {
1112
+ for (const change of changes) {
1113
+ if (change.type === "children") {
1114
+ change.removed.forEach((element) => elements.delete(element));
1115
+ change.added.forEach((element) => elements.add(element));
1116
+ } else if (change.type === "attribute") {
1117
+ if (change.element.matches(SCROLL)) elements.add(change.element);
1118
+ else elements.delete(change.element);
1119
+ }
1120
+ }
1121
+ },
1122
+ stop: () => {
1123
+ window.removeEventListener("scroll", onScroll, { capture: true });
1124
+ if (frame !== void 0) cancelAnimationFrame(frame);
1125
+ }
1126
+ };
1127
+ }
1128
+ });
1129
+
1130
+ // src/tracking/dom/videos.ts
1131
+ var VIDEO = roleSelector("video");
1132
+ var VIDEO_MILESTONES = [10, 25, 50, 75];
1133
+ var toSeconds = (seconds) => Number.isFinite(seconds) ? Math.round(seconds) : void 0;
1134
+ var toVideo = (video, container) => ({
1135
+ ...getCustomFields(container, "video"),
1136
+ videoTitle: getField(container, "title") || video.title || video.getAttribute("aria-label") || void 0,
1137
+ videoUrl: video.currentSrc || void 0,
1138
+ videoProvider: "html5",
1139
+ videoDuration: toSeconds(video.duration),
1140
+ videoCurrentTime: toSeconds(video.currentTime)
1141
+ });
1142
+ var videos = () => definePlugin({
1143
+ name: "videos",
1144
+ events: {
1145
+ videoStarted: "A tracked video started playing.",
1146
+ videoProgressed: "A tracked video passed 10, 25, 50 or 75% of its duration.",
1147
+ videoCompleted: "A tracked video played to its end."
1148
+ },
1149
+ install: ({ emit }) => {
1150
+ const videos2 = /* @__PURE__ */ new WeakMap();
1151
+ const onMedia = (event) => {
1152
+ const target = event.target;
1153
+ if (!(target instanceof HTMLVideoElement)) return;
1154
+ if (event.type !== "play" && !videos2.has(target)) return;
1155
+ const container = target.closest(VIDEO);
1156
+ if (!container) return;
1157
+ const reached = videos2.get(target);
1158
+ if (event.type === "emptied") {
1159
+ videos2.delete(target);
1160
+ } else if (event.type === "play" && !reached) {
1161
+ videos2.set(target, /* @__PURE__ */ new Set());
1162
+ emit("videoStarted", toVideo(target, container), target);
1163
+ } else if (event.type === "timeupdate" && reached && target.duration) {
1164
+ const percent = target.currentTime / target.duration * 100;
1165
+ for (const milestone of VIDEO_MILESTONES) {
1166
+ if (percent < milestone || reached.has(milestone)) continue;
1167
+ reached.add(milestone);
1168
+ emit(
1169
+ "videoProgressed",
1170
+ { ...toVideo(target, container), videoPercent: milestone },
1171
+ target
1172
+ );
1173
+ }
1174
+ } else if (event.type === "ended" && reached) {
1175
+ videos2.delete(target);
1176
+ emit(
1177
+ "videoCompleted",
1178
+ { ...toVideo(target, container), videoPercent: 100 },
1179
+ target
1180
+ );
1181
+ }
1182
+ };
1183
+ return {
1184
+ listeners: [
1185
+ ["play", onMedia],
1186
+ ["timeupdate", onMedia],
1187
+ ["ended", onMedia],
1188
+ ["emptied", onMedia]
1189
+ ]
1190
+ };
1191
+ }
1192
+ });
1193
+
1194
+ export { clicks, createDomWatcher, ecommerce, forms, pageViews, scroll, videos };
1195
+ //# sourceMappingURL=index.js.map
1196
+ //# sourceMappingURL=index.js.map