crunchit
Version:
Autotrack the events from your users
248 lines (222 loc) • 8.45 kB
JavaScript
function removeEmptyFields(obj) {
const isEmpty = (value) => {
if (Array.isArray(value)) return value.length === 0;
if (typeof value === "object" && value !== null)
return Object.keys(value).length === 0;
return value === "" || value === null || value === undefined;
};
const cleanedObj = {};
for (const key in obj) {
if (!isEmpty(obj[key])) {
cleanedObj[key] = obj[key];
}
}
return cleanedObj;
}
// ! Utility function to recursively get text from a node
function getTextFromNode(node, depth = 0) {
if (depth > 5) {
return "";
}
for (let child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== "") {
return child.textContent.trim();
} else if (child.nodeType === Node.ELEMENT_NODE) {
let text = getTextFromNode(child, depth + 1);
if (text !== "") {
return text;
}
}
}
return "";
}
// ! Main function to get intent
function getIntent(event, eventType = "clicked", element = null) {
let allVariables = {};
allVariables.eventType = eventType == "clicked" ? "CLICK" : "SCROLL";
var target = element || event.target;
// const { target } = event;
// ! Start with a default description
let intent = `User ${eventType} an element: [${target.tagName.toLowerCase()}]`;
allVariables.element = target.tagName.toLowerCase();
// ! Aria-label can be a good source of descriptive information
if (target.getAttribute("aria-label")) {
intent += ` with aria-label "${target.getAttribute("aria-label")}"`;
allVariables.aria = target.getAttribute("aria-label");
allVariables.text = target.getAttribute("aria-label");
}
// ! Title attribute can also be descriptive
if (target.getAttribute("title")) {
intent += ` with title "${target.getAttribute("title")}"`;
allVariables.title = target.getAttribute("title");
allVariables.text = target.getAttribute("title");
}
// ! Handle various element types
switch (target.tagName.toLowerCase()) {
case "button":
// ! If it's a button, we add that information. If it's a submit button, we also note that it submits a form.
allVariables.buttonText = target.innerText;
allVariables.text = target.innerText;
intent +=
target.type === "submit"
? `, with inner-text: ${target.innerText}, which submits a form`
: `, with inner-text: ${target.innerText}, which is a button`;
break;
case "a":
// ! If it's a link, we add the href.
allVariables.linkHref = target.href;
allVariables.text = target.href;
intent += `, which links to "${target.href}"`;
break;
case "option":
// ! If it's an option within a select, we note that and add the select name and option value.
const selectElement = target.parentElement;
allVariables.optionValue = target.value;
allVariables.text = target.value;
allVariables.optionTitle = selectElement.name;
intent += `, which is an option with value "${target.value}" of a select element with name "${selectElement.name}"`;
break;
case "input":
allVariables.inputType = target.type;
allVariables.inputChecked = `${target.checked}`;
allVariables.inputPlaceholder = target.placeholder;
allVariables.text = target.placeholder || target.type;
// ! If it's an input, we add details based on the type
switch (target.type) {
case "checkbox":
case "radio":
intent += `, which is a ${target.type} and is ${
target.checked ? "checked" : "not checked"
}`;
break;
case "submit":
intent += ", which is a submit button for a form";
break;
default:
intent += `, which is an input of type "${target.type}" and placeholder, "${target.placeholder}"`;
}
break;
case "textarea":
allVariables.inputType = "textarea";
// ! If it's a textarea, we note that.
intent += ", which is a textarea";
break;
case "img":
case "svg":
case "path":
case "g":
// ! SVG elements clicked. Traversing to nearest div, span, button or a
let element = target;
let associatedText = null;
// ! Function to find a text node within an element and its children
const findTextNode = (el) => {
if (el.nodeType === Node.TEXT_NODE && el.textContent.trim() !== "") {
return el.textContent.trim();
}
for (let child of el.childNodes) {
let text = findTextNode(child);
if (text) return text;
}
return null;
};
// ! Function to get the content of pseudo-elements
const getPseudoElementContent = (el, pseudoElement) => {
let content = window
.getComputedStyle(el, pseudoElement)
.getPropertyValue("content");
return content && content !== "none" ? content.slice(1, -1) : null; // Remove quotes
};
// ! Traverse until we find a text node, or we reach the root
while (element && !associatedText) {
// ! Check if the current element has a text node
associatedText = findTextNode(element);
// ! Check if the current element's pseudo-elements have content
if (!associatedText) {
associatedText =
getPseudoElementContent(element, "::before") ||
getPseudoElementContent(element, "::after");
}
// ! Check if element's siblings have a text node
if (!associatedText) {
let sibling = element.nextElementSibling;
while (sibling) {
associatedText = findTextNode(sibling);
if (associatedText) break;
sibling = sibling.nextElementSibling;
}
}
if (!associatedText) {
let sibling = element.previousElementSibling;
while (sibling) {
associatedText = findTextNode(sibling);
if (associatedText) break;
sibling = sibling.previousElementSibling;
}
}
// ! Move to the next level
if (!associatedText) {
element = element.parentElement;
}
}
if (associatedText) {
allVariables.associatedText = associatedText;
allVariables.text = associatedText;
intent += `, which is an element associated with the text "${associatedText}". `;
}
if (element.alt) {
allVariables.alt = element.alt;
intent += `, which has an alt ${element.alt} `;
}
break;
case "div":
case "span":
// ! If it's a div or a span with a role of button, we note that.
if (target.getAttribute("role") === "button") {
intent += `, which is a ${target.tagName.toLowerCase()} with a role of button`;
} else {
// ! If it's just a div or a span, we try to get some descriptive information from it.
// ! We look for an image, and for text up to three levels deep.
let description = ` which is a ${target.tagName.toLowerCase()}`;
const img = target.querySelector("img");
const prominentText = getTextFromNode(target);
allVariables.associatedText = prominentText;
allVariables.text = prominentText;
if (img) {
description += `, containing an image with src "${img.src}"`;
}
if (prominentText) {
description += `, containing the text "${prominentText}"`;
}
intent += description;
}
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "p":
// If it's a heading or a paragraph, we add the text content.
intent += `, which is a ${target.tagName.toLowerCase()} with text "${
target.textContent
}"`;
allVariables.associatedText = target.textContent;
allVariables.text = target.textContent;
break;
case "form":
// ! If it's a form, we note that.
intent += ", which is a form";
break;
case "fieldset":
// ! If it's a fieldset, we note that.
intent += ", which is a fieldset";
break;
case "label":
// ! If it's a label, we add the text content of the label.
intent += `, which is a label with text "${target.textContent}"`;
break;
}
return { intent, allVariables: removeEmptyFields(allVariables) };
}
module.exports = getIntent;