@drevops/axel 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +504 -2
- package/build/axel.drupal.gtag.js +423 -0
- package/build/axel.drupal.gtag.min.js +1 -0
- package/build/axel.drupal.gtm.js +415 -0
- package/build/axel.drupal.gtm.min.js +1 -0
- package/build/axel.drupal.js +381 -0
- package/build/axel.drupal.min.js +1 -0
- package/build/axel.esm.js +420 -0
- package/build/axel.gtag.js +418 -0
- package/build/axel.gtag.min.js +1 -0
- package/build/axel.gtm.js +410 -0
- package/build/axel.gtm.min.js +1 -0
- package/build/axel.js +376 -0
- package/build/axel.min.js +1 -0
- package/package.json +67 -5
- package/src/adapters/ga4.js +60 -0
- package/src/adapters/gtag.js +27 -0
- package/src/adapters/gtm.js +13 -0
- package/src/core/core.js +227 -0
- package/src/core/dom.js +153 -0
- package/src/core/event-types/click.js +130 -0
- package/src/core/event-types/field-complete.js +49 -0
- package/src/core/event-types/form-start.js +27 -0
- package/src/core/event-types/form-submit.js +45 -0
- package/src/index.js +8 -0
- package/src/integrations/drupal.js +31 -0
- package/src/integrations/standalone.js +27 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// src/core/dom.js
|
|
3
|
+
var PARAM_PREFIX = "data-axel-param-";
|
|
4
|
+
var CONTACT_PROTOCOLS = ["mailto:", "tel:", "sms:"];
|
|
5
|
+
function listen(type, handler) {
|
|
6
|
+
document.addEventListener(type, handler, true);
|
|
7
|
+
return () => document.removeEventListener(type, handler, true);
|
|
8
|
+
}
|
|
9
|
+
function getText(node) {
|
|
10
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
11
|
+
return node.textContent;
|
|
12
|
+
}
|
|
13
|
+
if (node.nodeType !== Node.ELEMENT_NODE || node.getAttribute("aria-hidden") === "true" || node.matches("select, textarea")) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
return [...node.childNodes].map(getText).join("");
|
|
17
|
+
}
|
|
18
|
+
function getLabel(element) {
|
|
19
|
+
const isButton = element instanceof HTMLInputElement && ["submit", "button", "reset"].includes(element.type);
|
|
20
|
+
const value = isButton ? element.value : getText(element);
|
|
21
|
+
return value.replace(/\s+/g, " ").trim() || element.getAttribute("aria-label") || element.getAttribute("title") || "";
|
|
22
|
+
}
|
|
23
|
+
function collectParams(element) {
|
|
24
|
+
const params = {};
|
|
25
|
+
for (let node = element; node; node = node.parentElement) {
|
|
26
|
+
[...node.attributes].filter((attribute) => attribute.name.startsWith(PARAM_PREFIX)).forEach((attribute) => {
|
|
27
|
+
const name = attribute.name.slice(PARAM_PREFIX.length).replace(/-/g, "_");
|
|
28
|
+
if (params[name] === void 0) {
|
|
29
|
+
params[name] = attribute.value;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return params;
|
|
34
|
+
}
|
|
35
|
+
function isSkipped(element) {
|
|
36
|
+
return element.closest("[data-axel-skip]") !== null;
|
|
37
|
+
}
|
|
38
|
+
function resolveUrl(element, attribute, fullUrl = false) {
|
|
39
|
+
let url;
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(element.getAttribute(attribute), element.baseURI);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
if (fullUrl) {
|
|
46
|
+
return url.href;
|
|
47
|
+
}
|
|
48
|
+
if (CONTACT_PROTOCOLS.includes(url.protocol)) {
|
|
49
|
+
return url.protocol;
|
|
50
|
+
}
|
|
51
|
+
url.search = "";
|
|
52
|
+
return url.href;
|
|
53
|
+
}
|
|
54
|
+
function formParams(form, fullUrl = false) {
|
|
55
|
+
const params = { form_name: form.getAttribute("data-axel-form") };
|
|
56
|
+
const id = form.getAttribute("id");
|
|
57
|
+
if (id) {
|
|
58
|
+
params.form_id = id;
|
|
59
|
+
}
|
|
60
|
+
const destination = form instanceof HTMLFormElement && form.hasAttribute("action") ? resolveUrl(form, "action", fullUrl) : void 0;
|
|
61
|
+
if (destination !== void 0) {
|
|
62
|
+
params.form_destination = destination;
|
|
63
|
+
}
|
|
64
|
+
return params;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/core/event-types/click.js
|
|
68
|
+
function findTitle(element, scope, selector) {
|
|
69
|
+
return [...scope.querySelectorAll(selector)].filter((title) => title.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING && !title.contains(element) && !isSkipped(title)).pop();
|
|
70
|
+
}
|
|
71
|
+
function getGroupTitle(element, kind, selectors) {
|
|
72
|
+
const group = element.closest(`[data-axel-${kind}]`);
|
|
73
|
+
if (!group) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const scopeSelector = group.getAttribute(`data-axel-${kind}-scope`);
|
|
77
|
+
const scope = scopeSelector && group.closest(scopeSelector) || group;
|
|
78
|
+
for (const selector of selectors) {
|
|
79
|
+
const title = findTitle(element, scope, selector);
|
|
80
|
+
if (title) {
|
|
81
|
+
return getLabel(title);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
function getSubmitDestination(element, fullUrl) {
|
|
87
|
+
if (element.type !== "submit" || !element.form) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
if (element.hasAttribute("formaction")) {
|
|
91
|
+
return resolveUrl(element, "formaction", fullUrl);
|
|
92
|
+
}
|
|
93
|
+
return element.form.hasAttribute("action") ? resolveUrl(element.form, "action", fullUrl) : void 0;
|
|
94
|
+
}
|
|
95
|
+
function click(axel) {
|
|
96
|
+
const track = (event) => {
|
|
97
|
+
const element = event.target instanceof Element ? event.target.closest(axel.settings.clickSelector) : null;
|
|
98
|
+
if (!element || isSkipped(element)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const enrolled = element.closest("[data-axel-event]");
|
|
102
|
+
if (!enrolled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const isLink = element instanceof HTMLAnchorElement;
|
|
106
|
+
const params = { link_text: getLabel(element), click_type: isLink ? "link" : "button" };
|
|
107
|
+
const { fullUrls } = axel.settings;
|
|
108
|
+
const linkUrl = isLink ? resolveUrl(element, "href", fullUrls) : getSubmitDestination(element, fullUrls);
|
|
109
|
+
if (linkUrl !== void 0) {
|
|
110
|
+
params.link_url = linkUrl;
|
|
111
|
+
}
|
|
112
|
+
const groupTitle = getGroupTitle(element, "group", ["[data-axel-group-title]", axel.settings.groupTitleSelector]);
|
|
113
|
+
if (groupTitle !== void 0) {
|
|
114
|
+
params.group_title = groupTitle;
|
|
115
|
+
}
|
|
116
|
+
const subgroupTitle = getGroupTitle(element, "subgroup", ["[data-axel-subgroup-title]"]);
|
|
117
|
+
if (subgroupTitle !== void 0) {
|
|
118
|
+
params.subgroup_title = subgroupTitle;
|
|
119
|
+
}
|
|
120
|
+
axel.track(enrolled.getAttribute("data-axel-event"), element, params);
|
|
121
|
+
};
|
|
122
|
+
const removers = [
|
|
123
|
+
listen("click", track),
|
|
124
|
+
// A middle click opens a link in a new tab and fires only 'auxclick'.
|
|
125
|
+
listen("auxclick", (event) => event.button === 1 && track(event))
|
|
126
|
+
];
|
|
127
|
+
return () => removers.forEach((remove) => remove());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/core/event-types/field-complete.js
|
|
131
|
+
function getFieldLabel(field) {
|
|
132
|
+
var _a, _b;
|
|
133
|
+
const legend = ["checkbox", "radio"].includes(field.type) ? (_a = field.closest("fieldset")) == null ? void 0 : _a.querySelector(":scope > legend") : null;
|
|
134
|
+
const label = legend || (field.type === "radio" ? null : (_b = field.labels) == null ? void 0 : _b[0]);
|
|
135
|
+
return (label ? getText(label).replace(/\s+/g, " ").trim() : "") || field.getAttribute("aria-label") || "";
|
|
136
|
+
}
|
|
137
|
+
function fieldComplete(axel) {
|
|
138
|
+
return listen("change", (event) => {
|
|
139
|
+
const field = event.target;
|
|
140
|
+
const form = field instanceof Element ? field.closest("[data-axel-form]") : null;
|
|
141
|
+
if (!form || isSkipped(field)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const params = formParams(form, axel.settings.fullUrls);
|
|
145
|
+
if (field.name) {
|
|
146
|
+
params.field_name = field.name;
|
|
147
|
+
}
|
|
148
|
+
params.field_label = getFieldLabel(field);
|
|
149
|
+
axel.track("field_complete", field, params);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/core/event-types/form-start.js
|
|
154
|
+
function formStart(axel) {
|
|
155
|
+
const started = /* @__PURE__ */ new WeakSet();
|
|
156
|
+
return listen("focusin", (event) => {
|
|
157
|
+
const form = event.target instanceof Element ? event.target.closest("[data-axel-form]") : null;
|
|
158
|
+
if (!form || started.has(form) || isSkipped(event.target)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
started.add(form);
|
|
162
|
+
axel.track("form_start", form, formParams(form, axel.settings.fullUrls));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/core/event-types/form-submit.js
|
|
167
|
+
var MARKER = "[data-axel-form-submitted]";
|
|
168
|
+
function formSubmit(axel) {
|
|
169
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
170
|
+
const report = (marker) => {
|
|
171
|
+
if (seen.has(marker) || isSkipped(marker)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
seen.add(marker);
|
|
175
|
+
axel.track("form_submit", marker, { form_name: marker.getAttribute("data-axel-form-submitted") });
|
|
176
|
+
};
|
|
177
|
+
const scan = (root) => {
|
|
178
|
+
var _a;
|
|
179
|
+
if ((_a = root.matches) == null ? void 0 : _a.call(root, MARKER)) {
|
|
180
|
+
report(root);
|
|
181
|
+
}
|
|
182
|
+
root.querySelectorAll(MARKER).forEach(report);
|
|
183
|
+
};
|
|
184
|
+
const observer = new MutationObserver((records) => {
|
|
185
|
+
records.forEach((record) => record.addedNodes.forEach((node) => node instanceof Element && scan(node)));
|
|
186
|
+
});
|
|
187
|
+
observer.observe(document, { childList: true, subtree: true });
|
|
188
|
+
scan(document);
|
|
189
|
+
return () => observer.disconnect();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/core/core.js
|
|
193
|
+
var TYPES = {
|
|
194
|
+
click,
|
|
195
|
+
form_start: formStart,
|
|
196
|
+
field_complete: fieldComplete,
|
|
197
|
+
form_submit: formSubmit
|
|
198
|
+
};
|
|
199
|
+
var DEFAULT_SETTINGS = {
|
|
200
|
+
// Event types to track, as keys of TYPES.
|
|
201
|
+
types: ["click"],
|
|
202
|
+
// An object with send(name, params, context) and an optional
|
|
203
|
+
// validate(name, params), or a function used as send().
|
|
204
|
+
adapter: null,
|
|
205
|
+
// Static parameters added to every event, overridden by all others.
|
|
206
|
+
properties: {},
|
|
207
|
+
// Elements whose clicks are tracked.
|
|
208
|
+
clickSelector: 'a[href], button, input[type="submit"], input[type="button"], input[type="reset"]',
|
|
209
|
+
// Headings that title a data-axel-group when it has no marker.
|
|
210
|
+
groupTitleSelector: "h2, h3, h4",
|
|
211
|
+
// Functions named by data-axel-collector, as name => function.
|
|
212
|
+
collectors: {},
|
|
213
|
+
// Keep query strings and contact addresses in link_url and form_destination.
|
|
214
|
+
fullUrls: false,
|
|
215
|
+
debugHash: "#axel-debug",
|
|
216
|
+
debugStorageKey: "axelDebug",
|
|
217
|
+
// Used by the standalone integration only.
|
|
218
|
+
autoInit: true
|
|
219
|
+
};
|
|
220
|
+
var Axel = class {
|
|
221
|
+
/**
|
|
222
|
+
* @param {object} settings - Overrides for DEFAULT_SETTINGS.
|
|
223
|
+
*/
|
|
224
|
+
constructor(settings = {}) {
|
|
225
|
+
var _a;
|
|
226
|
+
this.settings = { ...DEFAULT_SETTINGS, ...settings };
|
|
227
|
+
const { adapter } = this.settings;
|
|
228
|
+
this.adapter = typeof adapter === "function" ? { send: adapter } : adapter;
|
|
229
|
+
if (typeof ((_a = this.adapter) == null ? void 0 : _a.send) !== "function") {
|
|
230
|
+
throw new Error("The analytics adapter must be a function or an object with a send() function.");
|
|
231
|
+
}
|
|
232
|
+
this.settings.types.forEach((type) => {
|
|
233
|
+
if (!Object.keys(TYPES).includes(type)) {
|
|
234
|
+
throw new Error(`Unknown analytics event type "${type}".`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
Object.entries(this.settings.collectors).forEach(([name, collector]) => {
|
|
238
|
+
if (typeof collector !== "function") {
|
|
239
|
+
throw new Error(`The analytics collector "${name}" must be a function.`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
this.cleanups = null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Start tracking the configured event types.
|
|
246
|
+
*
|
|
247
|
+
* Calling it again on an initialized instance does nothing.
|
|
248
|
+
*
|
|
249
|
+
* @return {Axel} The instance.
|
|
250
|
+
*/
|
|
251
|
+
init() {
|
|
252
|
+
if (!this.cleanups) {
|
|
253
|
+
this.cleanups = this.settings.types.map((type) => TYPES[type](this));
|
|
254
|
+
}
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Stop tracking. The instance can be initialized again afterwards.
|
|
259
|
+
*/
|
|
260
|
+
destroy() {
|
|
261
|
+
var _a;
|
|
262
|
+
(_a = this.cleanups) == null ? void 0 : _a.forEach((cleanup) => cleanup());
|
|
263
|
+
this.cleanups = null;
|
|
264
|
+
if (shared === this) {
|
|
265
|
+
shared = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Send an event with the parameters of the element and its ancestors.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} name - The event name.
|
|
272
|
+
* @param {Element} element - The element the event is about.
|
|
273
|
+
* @param {object} derived - Parameters derived from the element, which
|
|
274
|
+
* 'data-axel-param-*' attributes override.
|
|
275
|
+
*/
|
|
276
|
+
track(name, element, derived = {}) {
|
|
277
|
+
const params = { ...derived, ...collectParams(element) };
|
|
278
|
+
this.send(name, { ...params, ...this.collect(name, params, element) });
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Run the collector named by the closest 'data-axel-collector'.
|
|
282
|
+
*
|
|
283
|
+
* A failing collector adds nothing, so the event is still sent.
|
|
284
|
+
*
|
|
285
|
+
* @param {string} name - The event name.
|
|
286
|
+
* @param {object} params - The event parameters, passed as a copy.
|
|
287
|
+
* @param {Element} element - The element the event is about.
|
|
288
|
+
* @return {object} The parameters the collector returns.
|
|
289
|
+
*/
|
|
290
|
+
collect(name, params, element) {
|
|
291
|
+
const source = element.closest("[data-axel-collector]");
|
|
292
|
+
if (!source) {
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
const collectorName = source.getAttribute("data-axel-collector");
|
|
296
|
+
try {
|
|
297
|
+
if (!Object.hasOwn(this.settings.collectors, collectorName)) {
|
|
298
|
+
throw new Error(`Unknown analytics collector "${collectorName}".`);
|
|
299
|
+
}
|
|
300
|
+
const collected = this.settings.collectors[collectorName](name, { ...params }, { element, source });
|
|
301
|
+
if (collected === void 0 || collected === null) {
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
if (typeof collected !== "object" || Array.isArray(collected)) {
|
|
305
|
+
throw new Error(`The analytics collector "${collectorName}" must return an object.`);
|
|
306
|
+
}
|
|
307
|
+
return collected;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(`[axel] ${error.message}`);
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Send a 'form_submit' event for a submission the server has confirmed.
|
|
315
|
+
*
|
|
316
|
+
* @param {string} formName - The form name.
|
|
317
|
+
* @param {object} params - Additional parameters.
|
|
318
|
+
*/
|
|
319
|
+
formSubmitted(formName, params = {}) {
|
|
320
|
+
this.send("form_submit", { form_name: formName, ...params });
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Send an event to the adapter.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} name - The event name.
|
|
326
|
+
* @param {object} params - The event parameters.
|
|
327
|
+
*/
|
|
328
|
+
send(name, params) {
|
|
329
|
+
var _a, _b;
|
|
330
|
+
const event = { ...this.settings.properties, ...params };
|
|
331
|
+
const debug = this.isDebug();
|
|
332
|
+
if (debug) {
|
|
333
|
+
console.log("[axel]", name, event);
|
|
334
|
+
(((_b = (_a = this.adapter).validate) == null ? void 0 : _b.call(_a, name, event)) || []).forEach((warning) => console.warn(`[axel] ${name}: ${warning}`));
|
|
335
|
+
}
|
|
336
|
+
this.adapter.send(name, event, { debug, settings: this.settings });
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check whether events are logged for debugging.
|
|
340
|
+
*
|
|
341
|
+
* The debug hash turns logging on until the browser tab closes, so it holds
|
|
342
|
+
* on the pages that clicks open.
|
|
343
|
+
*
|
|
344
|
+
* @return {boolean} TRUE if debugging.
|
|
345
|
+
*/
|
|
346
|
+
isDebug() {
|
|
347
|
+
const { debugHash, debugStorageKey } = this.settings;
|
|
348
|
+
const requested = window.location.hash === debugHash;
|
|
349
|
+
try {
|
|
350
|
+
if (requested) {
|
|
351
|
+
window.sessionStorage.setItem(debugStorageKey, "1");
|
|
352
|
+
}
|
|
353
|
+
return requested || window.sessionStorage.getItem(debugStorageKey) === "1";
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return requested;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var shared = null;
|
|
360
|
+
function init(settings = {}) {
|
|
361
|
+
shared = shared || new Axel(settings);
|
|
362
|
+
return shared.init();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/integrations/drupal.js
|
|
366
|
+
function start(adapter) {
|
|
367
|
+
window.Axel = Axel;
|
|
368
|
+
window.Drupal.behaviors.axel = {
|
|
369
|
+
// Listeners are delegated to the document and init() is idempotent, so
|
|
370
|
+
// every attach after the first, including AJAX ones, is a no-op.
|
|
371
|
+
attach() {
|
|
372
|
+
const settings = window.drupalSettings.axel;
|
|
373
|
+
const collectors = { ...window.axelCollectors, ...settings == null ? void 0 : settings.collectors };
|
|
374
|
+
window.axel = init({ ...settings, collectors, adapter: adapter || window.axelAdapter });
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/adapters/ga4.js
|
|
380
|
+
var LIMITS = {
|
|
381
|
+
eventName: 40,
|
|
382
|
+
params: 25,
|
|
383
|
+
paramValue: 100
|
|
384
|
+
};
|
|
385
|
+
function withDebugMode(params, debug) {
|
|
386
|
+
return debug ? { ...params, debug_mode: true } : params;
|
|
387
|
+
}
|
|
388
|
+
function validate(name, params) {
|
|
389
|
+
const warnings = [];
|
|
390
|
+
if (name.length > LIMITS.eventName) {
|
|
391
|
+
warnings.push(`event name is longer than ${LIMITS.eventName} characters.`);
|
|
392
|
+
}
|
|
393
|
+
if (Object.keys(params).length > LIMITS.params) {
|
|
394
|
+
warnings.push(`more than ${LIMITS.params} parameters.`);
|
|
395
|
+
}
|
|
396
|
+
Object.entries(params).filter(([, value]) => String(value).length > LIMITS.paramValue).forEach(([key]) => warnings.push(`"${key}" is longer than ${LIMITS.paramValue} characters.`));
|
|
397
|
+
return warnings;
|
|
398
|
+
}
|
|
399
|
+
function getDataLayer(settings) {
|
|
400
|
+
const name = settings.dataLayerName || "dataLayer";
|
|
401
|
+
window[name] = window[name] || [];
|
|
402
|
+
return window[name];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/adapters/gtm.js
|
|
406
|
+
var gtm = {
|
|
407
|
+
send(name, params, { debug, settings }) {
|
|
408
|
+
getDataLayer(settings).push({ ...withDebugMode(params, debug), event: name });
|
|
409
|
+
},
|
|
410
|
+
validate
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// axel.drupal.gtm.js
|
|
414
|
+
start(gtm);
|
|
415
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var E="data-axel-param-",P=["mailto:","tel:","sms:"];function l(t,e){return document.addEventListener(t,e,!0),()=>document.removeEventListener(t,e,!0)}function m(t){return t.nodeType===Node.TEXT_NODE?t.textContent:t.nodeType!==Node.ELEMENT_NODE||t.getAttribute("aria-hidden")==="true"||t.matches("select, textarea")?"":[...t.childNodes].map(m).join("")}function g(t){return(t instanceof HTMLInputElement&&["submit","button","reset"].includes(t.type)?t.value:m(t)).replace(/\s+/g," ").trim()||t.getAttribute("aria-label")||t.getAttribute("title")||""}function S(t){let e={};for(let n=t;n;n=n.parentElement)[...n.attributes].filter(r=>r.name.startsWith(E)).forEach(r=>{let o=r.name.slice(E.length).replace(/-/g,"_");e[o]===void 0&&(e[o]=r.value)});return e}function c(t){return t.closest("[data-axel-skip]")!==null}function f(t,e,n=!1){let r;try{r=new URL(t.getAttribute(e),t.baseURI)}catch(o){return}return n?r.href:P.includes(r.protocol)?r.protocol:(r.search="",r.href)}function h(t,e=!1){let n={form_name:t.getAttribute("data-axel-form")},r=t.getAttribute("id");r&&(n.form_id=r);let o=t instanceof HTMLFormElement&&t.hasAttribute("action")?f(t,"action",e):void 0;return o!==void 0&&(n.form_destination=o),n}function j(t,e,n){return[...e.querySelectorAll(n)].filter(r=>r.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING&&!r.contains(t)&&!c(r)).pop()}function T(t,e,n){let r=t.closest(`[data-axel-${e}]`);if(!r)return;let o=r.getAttribute(`data-axel-${e}-scope`),s=o&&r.closest(o)||r;for(let i of n){let a=j(t,s,i);if(a)return g(a)}return""}function C(t,e){if(!(t.type!=="submit"||!t.form))return t.hasAttribute("formaction")?f(t,"formaction",e):t.form.hasAttribute("action")?f(t.form,"action",e):void 0}function _(t){let e=r=>{let o=r.target instanceof Element?r.target.closest(t.settings.clickSelector):null;if(!o||c(o))return;let s=o.closest("[data-axel-event]");if(!s)return;let i=o instanceof HTMLAnchorElement,a={link_text:g(o),click_type:i?"link":"button"},{fullUrls:b}=t.settings,x=i?f(o,"href",b):C(o,b);x!==void 0&&(a.link_url=x);let w=T(o,"group",["[data-axel-group-title]",t.settings.groupTitleSelector]);w!==void 0&&(a.group_title=w);let y=T(o,"subgroup",["[data-axel-subgroup-title]"]);y!==void 0&&(a.subgroup_title=y),t.track(s.getAttribute("data-axel-event"),o,a)},n=[l("click",e),l("auxclick",r=>r.button===1&&e(r))];return()=>n.forEach(r=>r())}function R(t){var r,o;let n=(["checkbox","radio"].includes(t.type)?(r=t.closest("fieldset"))==null?void 0:r.querySelector(":scope > legend"):null)||(t.type==="radio"?null:(o=t.labels)==null?void 0:o[0]);return(n?m(n).replace(/\s+/g," ").trim():"")||t.getAttribute("aria-label")||""}function k(t){return l("change",e=>{let n=e.target,r=n instanceof Element?n.closest("[data-axel-form]"):null;if(!r||c(n))return;let o=h(r,t.settings.fullUrls);n.name&&(o.field_name=n.name),o.field_label=R(n),t.track("field_complete",n,o)})}function A(t){let e=new WeakSet;return l("focusin",n=>{let r=n.target instanceof Element?n.target.closest("[data-axel-form]"):null;!r||e.has(r)||c(n.target)||(e.add(r),t.track("form_start",r,h(r,t.settings.fullUrls)))})}var v="[data-axel-form-submitted]";function L(t){let e=new WeakSet,n=s=>{e.has(s)||c(s)||(e.add(s),t.track("form_submit",s,{form_name:s.getAttribute("data-axel-form-submitted")}))},r=s=>{var i;(i=s.matches)!=null&&i.call(s,v)&&n(s),s.querySelectorAll(v).forEach(n)},o=new MutationObserver(s=>{s.forEach(i=>i.addedNodes.forEach(a=>a instanceof Element&&r(a)))});return o.observe(document,{childList:!0,subtree:!0}),r(document),()=>o.disconnect()}var N={click:_,form_start:A,field_complete:k,form_submit:L},F={types:["click"],adapter:null,properties:{},clickSelector:'a[href], button, input[type="submit"], input[type="button"], input[type="reset"]',groupTitleSelector:"h2, h3, h4",collectors:{},fullUrls:!1,debugHash:"#axel-debug",debugStorageKey:"axelDebug",autoInit:!0},p=class{constructor(e={}){var r;this.settings={...F,...e};let{adapter:n}=this.settings;if(this.adapter=typeof n=="function"?{send:n}:n,typeof((r=this.adapter)==null?void 0:r.send)!="function")throw new Error("The analytics adapter must be a function or an object with a send() function.");this.settings.types.forEach(o=>{if(!Object.keys(N).includes(o))throw new Error(`Unknown analytics event type "${o}".`)}),Object.entries(this.settings.collectors).forEach(([o,s])=>{if(typeof s!="function")throw new Error(`The analytics collector "${o}" must be a function.`)}),this.cleanups=null}init(){return this.cleanups||(this.cleanups=this.settings.types.map(e=>N[e](this))),this}destroy(){var e;(e=this.cleanups)==null||e.forEach(n=>n()),this.cleanups=null,d===this&&(d=null)}track(e,n,r={}){let o={...r,...S(n)};this.send(e,{...o,...this.collect(e,o,n)})}collect(e,n,r){let o=r.closest("[data-axel-collector]");if(!o)return{};let s=o.getAttribute("data-axel-collector");try{if(!Object.hasOwn(this.settings.collectors,s))throw new Error(`Unknown analytics collector "${s}".`);let i=this.settings.collectors[s](e,{...n},{element:r,source:o});if(i==null)return{};if(typeof i!="object"||Array.isArray(i))throw new Error(`The analytics collector "${s}" must return an object.`);return i}catch(i){return console.error(`[axel] ${i.message}`),{}}}formSubmitted(e,n={}){this.send("form_submit",{form_name:e,...n})}send(e,n){var s,i;let r={...this.settings.properties,...n},o=this.isDebug();o&&(console.log("[axel]",e,r),(((i=(s=this.adapter).validate)==null?void 0:i.call(s,e,r))||[]).forEach(a=>console.warn(`[axel] ${e}: ${a}`))),this.adapter.send(e,r,{debug:o,settings:this.settings})}isDebug(){let{debugHash:e,debugStorageKey:n}=this.settings,r=window.location.hash===e;try{return r&&window.sessionStorage.setItem(n,"1"),r||window.sessionStorage.getItem(n)==="1"}catch(o){return r}}},d=null;function O(t={}){return d=d||new p(t),d.init()}function D(t){window.Axel=p,window.Drupal.behaviors.axel={attach(){let e=window.drupalSettings.axel,n={...window.axelCollectors,...e==null?void 0:e.collectors};window.axel=O({...e,collectors:n,adapter:t||window.axelAdapter})}}}var u={eventName:40,params:25,paramValue:100};function $(t,e){return e?{...t,debug_mode:!0}:t}function I(t,e){let n=[];return t.length>u.eventName&&n.push(`event name is longer than ${u.eventName} characters.`),Object.keys(e).length>u.params&&n.push(`more than ${u.params} parameters.`),Object.entries(e).filter(([,r])=>String(r).length>u.paramValue).forEach(([r])=>n.push(`"${r}" is longer than ${u.paramValue} characters.`)),n}function U(t){let e=t.dataLayerName||"dataLayer";return window[e]=window[e]||[],window[e]}var M={send(t,e,{debug:n,settings:r}){U(r).push({...$(e,n),event:t})},validate:I};D(M);})();
|