altcha
Version:
Privacy-first CAPTCHA widget, compliant with global regulations (GDPR/HIPAA/CCPA/LGDP/DPDPA/PIPL) and WCAG accessible. No tracking, self-verifying.
291 lines (290 loc) • 9.82 kB
JavaScript
var y = Object.defineProperty;
var p = (s) => {
throw TypeError(s);
};
var C = (s, t, e) => t in s ? y(s, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : s[t] = e;
var l = (s, t, e) => C(s, typeof t != "symbol" ? t + "" : t, e), F = (s, t, e) => t.has(s) || p("Cannot " + e);
var i = (s, t, e) => (F(s, t, "read from private field"), e ? e.call(s) : t.get(s)), r = (s, t, e) => t.has(s) ? p("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(s) : t.set(s, e), T = (s, t, e, n) => (F(s, t, "write to private field"), n ? n.call(s, e) : t.set(s, e), e), E = (s, t, e) => (F(s, t, "access private method"), e);
new TextEncoder();
function L() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
}
}
class v {
/**
* Constructs a new instance of the Plugin.
*
* @param {PluginContext} context - The context provided to the plugin, containing necessary configurations and dependencies.
*/
constructor(t) {
this.context = t;
}
/**
* Registers a plugin class in the global `altchaPlugins` array.
* Ensures the plugin is added only once.
*
* @param {new(context: PluginContext) => Plugin} cls - The plugin class to register.
*/
static register(t) {
typeof globalThis.altchaPlugins != "object" && (globalThis.altchaPlugins = []), globalThis.altchaPlugins.includes(t) || globalThis.altchaPlugins.push(t);
}
/**
* Clean up resources when the plugin is destroyed.
* Override this method in subclasses to implement custom destruction logic.
*/
destroy() {
}
/**
* Callback triggered when an error changes.
* Override this method in subclasses to handle error state changes.
*
* @param {string | null} err - The error message or `null` if there's no error.
*/
onErrorChange(t) {
}
/**
* Callback triggered when the plugin state changes.
* Override this method in subclasses to handle state changes.
*
* @param {State} state - The new state of the plugin.
*/
onStateChange(t) {
}
}
/**
* A distinct name of the plugin. Every plugin must have it's own name.
*/
l(v, "pluginName");
var o, a, m, f, w;
class A extends v {
/**
* Creates an instance of PluginAnalytics.
*
* @param {PluginContext} context - The context object containing plugin configurations.
*/
constructor(e) {
super(e);
r(this, f);
// HTML form element associated with the plugin
r(this, o);
// Session instance for tracking analytics data
r(this, a);
// Bound method for form submission handling
r(this, m, E(this, f, w).bind(this));
if (T(this, o, this.context.el.closest("form")), i(this, o)) {
let n = i(this, o).getAttribute("data-beacon-url");
const h = i(this, o).getAttribute("action");
!n && h && (n = h + "/beacon"), i(this, o).addEventListener("submit", i(this, m)), T(this, a, new N(i(this, o), n));
}
}
/**
* Destroys the plugin instance, removing event listeners and cleaning up the session.
*/
destroy() {
var e, n;
(e = i(this, o)) == null || e.removeEventListener("submit", i(this, m)), (n = i(this, a)) == null || n.destroy();
}
/**
* Tracks errors by forwarding them to the session instance.
*
* @param {string | null} err - The error message, or `null` if no error exists.
*/
onErrorChange(e) {
var n;
(n = i(this, a)) == null || n.trackError(e);
}
}
o = new WeakMap(), a = new WeakMap(), m = new WeakMap(), f = new WeakSet(), /**
* Handles form submission events, appending session data to the form if applicable.
*/
w = function() {
var e;
if (i(this, a) && !i(this, a).submitTime) {
i(this, a).end();
const n = i(this, a).dataAsBase64();
this.context.dispatch("session", n);
const h = document.createElement("input");
h.type = "hidden", h.name = "__session", h.value = n, (e = i(this, o)) == null || e.appendChild(h);
}
}, l(A, "pluginName", "analytics");
var c, u, d, g, b;
class N {
/**
* Creates a new Session instance.
*
* @param {HTMLFormElement} elForm - The form element being tracked.
* @param {string | null} [beaconUrl=null] - The URL to send analytics data to.
*/
constructor(t, e = null) {
// Error message associated with the session
l(this, "error", null);
// Timestamp when the form was loaded
l(this, "loadTime", Date.now());
// Timestamp when the form was submitted
l(this, "submitTime", null);
// Timestamp when the user started interacting with the form
l(this, "startTime", null);
// Minimum time in milliseconds required to consider the form "viewed"
l(this, "viewTimeThresholdMs", 1500);
// Tracks the number of changes made to each form field
r(this, c, {});
// Name of the last input field focused by the user
r(this, u, null);
// Bound method for handling form change events
r(this, d, this.onFormChange.bind(this));
// Bound method for handling form focus events
r(this, g, this.onFormFocus.bind(this));
// Bound method for handling the unload event
r(this, b, this.onUnload.bind(this));
this.elForm = t, this.beaconUrl = e, window.addEventListener("unload", i(this, b)), this.elForm.addEventListener("change", i(this, d)), this.elForm.addEventListener("focusin", i(this, g));
}
/**
* Collects and returns analytics data about the form interaction.
*
* @returns {Record<string, unknown>} - An object containing analytics data.
*/
data() {
const t = Object.entries(i(this, c));
return {
correction: t.length && t.filter(([e, n]) => n > 1).length / t.length || 0,
dropoff: !this.submitTime && !this.error && i(this, u) ? i(this, u) : null,
error: this.error,
mobile: this.isMobile(),
start: this.startTime,
submit: this.submitTime,
tz: L()
};
}
/**
* Encodes the session data as a base64 string.
*
* @returns {string} - The base64-encoded session data.
*/
dataAsBase64() {
try {
return btoa(JSON.stringify(this.data()));
} catch (t) {
console.error("failed to encode ALTCHA session data to base64", t);
}
return "";
}
/**
* Destroys the session, removing event listeners.
*/
destroy() {
window.removeEventListener("unload", i(this, b)), this.elForm.removeEventListener("change", i(this, d)), this.elForm.removeEventListener("focusin", i(this, g));
}
/**
* Marks the session as ended by recording the submission time.
*/
end() {
this.submitTime || (this.submitTime = Date.now());
}
/**
* Retrieves the name of a form field, including a group label if available.
*
* @param {HTMLInputElement} el - The input element.
* @param {number} [maxLength=40] - The maximum length of the field name.
* @returns {string} - The field name, truncated to `maxLength` if necessary.
*/
getFieldName(t, e = 40) {
const n = t.getAttribute("data-group-label"), h = t.getAttribute("name") || t.getAttribute("aria-label");
return ((n ? n + ": " : "") + h).slice(0, e);
}
/**
* Determines if the current device is a mobile device.
*
* @returns {boolean} - `true` if the device is mobile, otherwise `false`.
*/
isMobile() {
const t = "userAgentData" in navigator && navigator.userAgentData ? navigator.userAgentData : {};
return "mobile" in t ? t.mobile === !0 : /Mobi/i.test(window.navigator.userAgent);
}
/**
* Checks if a given element is an input element (input, select, or textarea).
*
* @param {HTMLElement} el - The element to check.
* @returns {boolean} - `true` if the element is an input, otherwise `false`.
*/
isInput(t) {
return ["INPUT", "SELECT", "TEXTAREA"].includes(t.tagName);
}
/**
* Tracks changes to a specific form field.
*
* @param {HTMLInputElement} el - The input element that changed.
*/
onFormFieldChange(t) {
const e = this.getFieldName(t);
e && this.trackFieldChange(e);
}
/**
* Handles form change events, tracking changes to input fields.
*
* @param {Event} ev - The change event.
*/
onFormChange(t) {
const e = t.target;
e && this.isInput(e) && this.onFormFieldChange(e);
}
/**
* Handles form focus events, marking the session start time and tracking the last focused field.
*
* @param {FocusEvent} ev - The focus event.
*/
onFormFocus(t) {
const e = t.target;
if (this.startTime || this.start(), e && this.isInput(e)) {
const n = this.getFieldName(e);
n && T(this, u, n);
}
}
/**
* Handles the window unload event, sending a beacon with session data if the form was viewed but not submitted.
*/
onUnload() {
this.loadTime <= Date.now() - this.viewTimeThresholdMs && !this.submitTime && this.sendBeacon();
}
/**
* Sends a beacon with session data to the specified beacon URL.
*/
async sendBeacon() {
if (this.beaconUrl && "sendBeacon" in navigator)
try {
navigator.sendBeacon(
new URL(this.beaconUrl, location.origin),
JSON.stringify(this.data())
);
} catch {
}
}
/**
* Marks the session as started by recording the start time.
*/
start() {
this.startTime = Date.now();
}
/**
* Tracks an error associated with the session.
*
* @param {string | null} err - The error message, or `null` if no error exists.
*/
trackError(t) {
this.error = t === null ? null : String(t);
}
/**
* Tracks a change to a specific form field.
*
* @param {string} name - The name of the form field.
*/
trackFieldChange(t) {
i(this, c)[t] = (i(this, c)[t] || 0) + 1;
}
}
c = new WeakMap(), u = new WeakMap(), d = new WeakMap(), g = new WeakMap(), b = new WeakMap();
v.register(A);
export {
A as PluginAnalytics
};