crunchit
Version:
Autotrack the events from your users
216 lines (192 loc) • 6.49 kB
JavaScript
const state = {
hoveredElement: null,
selectedElement: null,
hoveredLines: [],
selectedLines: [],
successfulSelections: [],
};
const config = {
hoveredColorDark: "mediumpurple",
selectedColorDark: "darkorange",
selectedColorLight: "rgba(255, 165, 0, 0.1)",
lineThickness: 3,
zIndex: "999999999",
boxShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)",
ignoreElementIds: ["CRUNCH_IFRAME_CONTAINER", "CRUNCH_NAVBAR_CONTAINER"],
};
function clearPreviousSelection() {
if (state.selectedElement) {
state.selectedElement.style.background = "";
}
state.selectedLines.forEach((line) => {
if (line.parentNode) {
line.parentNode.removeChild(line);
}
});
state.selectedLines.length = 0;
}
function unhighlightSelectedElement() {
clearPreviousSelection();
state.selectedElement = null;
}
function isInsideIgnoredElement(element) {
let parent = element;
while (parent) {
if (config.ignoreElementIds.includes(parent.id)) {
return true;
}
parent = parent.parentElement;
}
return false;
}
function createLine(color) {
const line = document.createElement("div");
line.style.position = "absolute";
line.style.backgroundColor = color;
line.style.zIndex = config.zIndex;
line.style.boxShadow = config.boxShadow;
return line;
}
function updateLinePositions(target, color, lineArray) {
const rect = target.getBoundingClientRect();
if (rect.height < 4 || rect.width < 4) {
return;
}
lineArray.forEach((line) => {
if (line.parentNode) {
line.parentNode.removeChild(line);
}
});
lineArray.length = 0;
let actualColor = rect.height < 12 || rect.width < 12 ? "orangered" : color;
["top", "bottom", "left", "right"].forEach((position) => {
const line = createLine(actualColor);
if (position === "top" || position === "bottom") {
line.style.height = `${config.lineThickness}px`;
line.style.left = `${rect.left}px`;
line.style.width =
position === "bottom" ? `${rect.width + 3}px` : `${rect.width}px`;
line.style.top =
position === "top" ? `${rect.top}px` : `${rect.bottom}px`;
} else {
line.style.width = `${config.lineThickness}px`;
line.style.top = `${rect.top}px`;
line.style.height = `${rect.height}px`;
line.style.left =
position === "left" ? `${rect.left}px` : `${rect.right}px`;
}
document.body.appendChild(line);
lineArray.push(line);
});
}
function highlightHoveredElement(event) {
if (isInsideIgnoredElement(event.target) || state.selectedElement) return;
state.hoveredElement = event.target;
updateLinePositions(
state.hoveredElement,
config.hoveredColorDark,
state.hoveredLines
);
}
function highlightSelectedElement(event) {
if (isInsideIgnoredElement(event.target)) return;
event.preventDefault();
event.stopImmediatePropagation();
clearPreviousSelection();
let selection = event.target;
selection.style.background = config.selectedColorLight;
state.selectedElement = selection;
updateLinePositions(
state.selectedElement,
config.selectedColorDark,
state.selectedLines
);
}
function activateHighlighter() {
unhighlightSelectedElement();
document.addEventListener("mouseover", highlightHoveredElement);
document.addEventListener("click", highlightSelectedElement, true);
}
function deactivateHighlighter() {
document.removeEventListener("mouseover", highlightHoveredElement);
document.removeEventListener("click", highlightSelectedElement, true);
// Optional: Clear any existing highlights
clearPreviousSelection();
state.hoveredLines.forEach((line) => {
if (line.parentNode) {
line.parentNode.removeChild(line);
}
});
state.hoveredLines.length = 0;
state.selectedLines.length = 0;
state.hoveredElement = null;
state.selectedElement = null;
}
function successfulSelection(details) {
if (state.selectedElement && state.selectedLines.length > 0) {
const rect = state.selectedElement.getBoundingClientRect();
const successfulLines = state.selectedLines.map((line, index) => {
const newLine = line.cloneNode();
newLine.style.backgroundColor = "lawngreen";
newLine.style.position = "absolute"; // Relative to the parent selectedElement
switch (index) {
case 0:
newLine.style.top = "0px"; // Relative to the parent selectedElement
newLine.style.left = "0px"; // Relative to the parent selectedElement
break;
case 1:
newLine.style.top = `${rect.height}px`; // Relative to the parent selectedElement
newLine.style.left = "0px"; // Relative to the parent selectedElement
break;
case 2:
newLine.style.top = "0px"; // Relative to the parent selectedElement
newLine.style.left = "0px"; // Relative to the parent selectedElement
break;
case 3:
newLine.style.top = "0px"; // Relative to the parent selectedElement
newLine.style.left = `${rect.width}px`; // Relative to the parent selectedElement
break;
default:
break;
}
return newLine;
});
// Add the successful selection to the state
state.successfulSelections.push({
element: state.selectedElement,
lines: successfulLines,
});
// Make the selected element's position relative so the lines will be relative to it
state.selectedElement.style.position = "relative";
// Append the lines to the selected element
successfulLines.forEach((line) => {
state.selectedElement.appendChild(line);
});
let successfulSelectionObject = { ...details, successfulLines };
let foundEntryIndex;
let foundEntry = state.successfulSelections.find((singleEntry, index) => {
if (singleEntry.xpath == details.xpath) {
foundEntryIndex = index;
return true;
}
return false;
});
if (foundEntry) {
let localSuccess = [...state.successfulSelections];
localSuccess[foundEntryIndex] = successfulSelectionObject;
state.successfulSelections = localSuccess;
} else state.successfulSelections.push(successfulSelectionObject);
}
}
const isAlreadySelected = (xpath) => {
return state.successfulSelections.find((singleSelection) => {
return singleSelection.xpath == xpath;
});
};
module.exports = {
activateHighlighter,
unhighlightSelectedElement,
deactivateHighlighter,
successfulSelection,
isAlreadySelected,
};