UNPKG

crunchit

Version:

Autotrack the events from your users

248 lines (222 loc) 8.45 kB
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;