feat: add child selector caching, revamp dom highlighting

This commit is contained in:
Rohit
2025-07-06 16:18:13 +05:30
parent 315cf944b8
commit 33d9522a67

View File

@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import { AuthContext } from '../../context/auth'; import { AuthContext } from '../../context/auth';
import { coordinateMapper } from '../../helpers/coordinateMapper'; import { coordinateMapper } from '../../helpers/coordinateMapper';
import { useBrowserDimensionsStore } from '../../context/browserDimensions'; import { useBrowserDimensionsStore } from '../../context/browserDimensions';
import { clientSelectorGenerator } from "../../helpers/clientSelectorGenerator"; import { clientSelectorGenerator, ElementFingerprint } from "../../helpers/clientSelectorGenerator";
import DatePicker from "../pickers/DatePicker"; import DatePicker from "../pickers/DatePicker";
import Dropdown from "../pickers/Dropdown"; import Dropdown from "../pickers/Dropdown";
import TimePicker from "../pickers/TimePicker"; import TimePicker from "../pickers/TimePicker";
@@ -147,7 +147,7 @@ export const BrowserWindow = () => {
const { browserWidth, browserHeight } = useBrowserDimensionsStore(); const { browserWidth, browserHeight } = useBrowserDimensionsStore();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined); const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>(""); const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null); const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], groupElements?: Array<{ element: HTMLElement; rect: DOMRect } >} | null>(null);
const [showAttributeModal, setShowAttributeModal] = useState(false); const [showAttributeModal, setShowAttributeModal] = useState(false);
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]); const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null); const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
@@ -156,6 +156,7 @@ export const BrowserWindow = () => {
const [isDOMMode, setIsDOMMode] = useState(false); const [isDOMMode, setIsDOMMode] = useState(false);
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(null); const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [cachedChildSelectors, setCachedChildSelectors] = useState<string[]>([]);
const [listSelector, setListSelector] = useState<string | null>(null); const [listSelector, setListSelector] = useState<string | null>(null);
const [fields, setFields] = useState<Record<string, TextStep>>({}); const [fields, setFields] = useState<Record<string, TextStep>>({});
@@ -168,6 +169,12 @@ export const BrowserWindow = () => {
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext(); const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps(); const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
const [currentGroupInfo, setCurrentGroupInfo] = useState<{
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
} | null>(null);
const { state } = useContext(AuthContext); const { state } = useContext(AuthContext);
const { user } = state; const { user } = state;
@@ -304,8 +311,23 @@ export const BrowserWindow = () => {
socket?.emit("listSelector", { selector: listSelector }); socket?.emit("listSelector", { selector: listSelector });
clientSelectorGenerator.setListSelector(listSelector); clientSelectorGenerator.setListSelector(listSelector);
setCachedChildSelectors([]);
if (currentSnapshot) {
const iframeElement = document.querySelector(
"#dom-browser-iframe"
) as HTMLIFrameElement;
if (iframeElement?.contentDocument) {
const childSelectors = clientSelectorGenerator.getChildSelectors(
iframeElement.contentDocument,
listSelector
);
setCachedChildSelectors(childSelectors);
} }
}, [isDOMMode, listSelector, socket, getList]); }
}
}, [isDOMMode, listSelector, socket, getList, currentSnapshot]);
useEffect(() => { useEffect(() => {
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height); coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
@@ -345,6 +367,7 @@ export const BrowserWindow = () => {
setListSelector(null); setListSelector(null);
setFields({}); setFields({});
setCurrentListId(null); setCurrentListId(null);
setCachedChildSelectors([]);
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -408,8 +431,19 @@ export const BrowserWindow = () => {
selector: string; selector: string;
elementInfo: ElementInfo | null; elementInfo: ElementInfo | null;
childSelectors?: string[]; childSelectors?: string[];
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
groupFingerprint: ElementFingerprint;
};
isDOMMode?: boolean; isDOMMode?: boolean;
}) => { }) => {
if (!getText && !getList) {
setHighlighterData(null);
return;
}
if (!isDOMMode || !currentSnapshot) { if (!isDOMMode || !currentSnapshot) {
return; return;
} }
@@ -424,15 +458,6 @@ export const BrowserWindow = () => {
) as HTMLIFrameElement; ) as HTMLIFrameElement;
} }
if (!iframeElement) {
const browserWindow = document.querySelector("#browser-window");
if (browserWindow) {
iframeElement = browserWindow.querySelector(
"iframe"
) as HTMLIFrameElement;
}
}
if (!iframeElement) { if (!iframeElement) {
console.error("Could not find iframe element for DOM highlighting"); console.error("Could not find iframe element for DOM highlighting");
return; return;
@@ -441,6 +466,12 @@ export const BrowserWindow = () => {
const iframeRect = iframeElement.getBoundingClientRect(); const iframeRect = iframeElement.getBoundingClientRect();
const IFRAME_BODY_PADDING = 16; const IFRAME_BODY_PADDING = 16;
if (data.groupInfo) {
setCurrentGroupInfo(data.groupInfo);
} else {
setCurrentGroupInfo(null);
}
const absoluteRect = new DOMRect( const absoluteRect = new DOMRect(
data.rect.x + iframeRect.left - IFRAME_BODY_PADDING, data.rect.x + iframeRect.left - IFRAME_BODY_PADDING,
data.rect.y + iframeRect.top - IFRAME_BODY_PADDING, data.rect.y + iframeRect.top - IFRAME_BODY_PADDING,
@@ -451,12 +482,36 @@ export const BrowserWindow = () => {
const mappedData = { const mappedData = {
...data, ...data,
rect: absoluteRect, rect: absoluteRect,
childSelectors: data.childSelectors || cachedChildSelectors,
}; };
if (getList === true) { if (getList === true) {
if (listSelector) { if (!listSelector && data.groupInfo?.isGroupElement) {
socket?.emit("listSelector", { selector: listSelector }); const updatedGroupElements = data.groupInfo.groupElements.map(
const hasValidChildSelectors = (element) => {
const elementRect = element.getBoundingClientRect();
return {
element,
rect: new DOMRect(
elementRect.x + iframeRect.left - IFRAME_BODY_PADDING,
elementRect.y + iframeRect.top - IFRAME_BODY_PADDING,
elementRect.width,
elementRect.height
),
};
}
);
const mappedData = {
...data,
rect: absoluteRect,
groupElements: updatedGroupElements,
childSelectors: data.childSelectors || cachedChildSelectors,
};
setHighlighterData(mappedData);
} else if (listSelector) {
const hasChildSelectors =
Array.isArray(mappedData.childSelectors) && Array.isArray(mappedData.childSelectors) &&
mappedData.childSelectors.length > 0; mappedData.childSelectors.length > 0;
@@ -471,62 +526,8 @@ export const BrowserWindow = () => {
} else { } else {
setHighlighterData(null); setHighlighterData(null);
} }
} else if ( } else if (hasChildSelectors) {
mappedData.childSelectors &&
mappedData.childSelectors.includes(mappedData.selector)
) {
setHighlighterData(mappedData); setHighlighterData(mappedData);
} else if (
mappedData.elementInfo?.isIframeContent &&
mappedData.childSelectors
) {
const isIframeChild = mappedData.childSelectors.some(
(childSelector) =>
mappedData.selector.includes(":>>") &&
childSelector
.split(":>>")
.some((part) => mappedData.selector.includes(part.trim()))
);
setHighlighterData(isIframeChild ? mappedData : null);
} else if (
mappedData.selector.includes(":>>") &&
hasValidChildSelectors
) {
const selectorParts = mappedData.selector
.split(":>>")
.map((part) => part.trim());
const isValidMixedSelector = selectorParts.some((part) =>
mappedData.childSelectors!.some((childSelector) =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? mappedData : null);
} else if (
mappedData.elementInfo?.isShadowRoot &&
mappedData.childSelectors
) {
const isShadowChild = mappedData.childSelectors.some(
(childSelector) =>
mappedData.selector.includes(">>") &&
childSelector
.split(">>")
.some((part) => mappedData.selector.includes(part.trim()))
);
setHighlighterData(isShadowChild ? mappedData : null);
} else if (
mappedData.selector.includes(">>") &&
hasValidChildSelectors
) {
const selectorParts = mappedData.selector
.split(">>")
.map((part) => part.trim());
const isValidMixedSelector = selectorParts.some((part) =>
mappedData.childSelectors!.some((childSelector) =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? mappedData : null);
} else { } else {
setHighlighterData(null); setHighlighterData(null);
} }
@@ -534,23 +535,29 @@ export const BrowserWindow = () => {
setHighlighterData(mappedData); setHighlighterData(mappedData);
} }
} else { } else {
// getText mode
setHighlighterData(mappedData); setHighlighterData(mappedData);
} }
}, },
[ [
isDOMMode, isDOMMode,
currentSnapshot, currentSnapshot,
getText,
getList, getList,
socket, socket,
listSelector, listSelector,
paginationMode, paginationMode,
paginationType, paginationType,
limitMode, limitMode,
cachedChildSelectors,
] ]
); );
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => { const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], isDOMMode?: boolean; }) => {
if (isDOMMode || data.isDOMMode) {
domHighlighterHandler(data);
return;
}
const now = performance.now(); const now = performance.now();
if (now - highlighterUpdateRef.current < 16) { if (now - highlighterUpdateRef.current < 16) {
return; return;
@@ -652,6 +659,20 @@ export const BrowserWindow = () => {
}; };
}, [socket, highlighterHandler, onMouseMove, getList, listSelector]); }, [socket, highlighterHandler, onMouseMove, getList, listSelector]);
useEffect(() => {
document.addEventListener("mousemove", onMouseMove, false);
if (socket) {
socket.off("highlighter", highlighterHandler);
socket.on("highlighter", highlighterHandler);
}
return () => {
document.removeEventListener("mousemove", onMouseMove);
if (socket) {
socket.off("highlighter", highlighterHandler);
}
};
}, [socket, highlighterHandler, getList, listSelector]);
useEffect(() => { useEffect(() => {
if (socket && listSelector) { if (socket && listSelector) {
console.log('Syncing list selector with server:', listSelector); console.log('Syncing list selector with server:', listSelector);
@@ -673,11 +694,205 @@ export const BrowserWindow = () => {
selector: string; selector: string;
elementInfo: ElementInfo | null; elementInfo: ElementInfo | null;
childSelectors?: string[]; childSelectors?: string[];
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
};
}) => { }) => {
setShowAttributeModal(false); setShowAttributeModal(false);
setSelectedElement(null); setSelectedElement(null);
setAttributeOptions([]); setAttributeOptions([]);
if (paginationMode && getList) {
if (
paginationType !== "" &&
paginationType !== "scrollDown" &&
paginationType !== "scrollUp" &&
paginationType !== "none"
) {
setPaginationSelector(highlighterData.selector);
notify(
`info`,
t(
"browser_window.attribute_modal.notifications.pagination_select_success"
)
);
addListStep(
listSelector!,
fields,
currentListId || 0,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: paginationType, selector: highlighterData.selector }
);
socket?.emit("setPaginationMode", { pagination: false });
}
return;
}
if (
getList === true &&
!listSelector &&
highlighterData.groupInfo?.isGroupElement
) {
let cleanedSelector = highlighterData.selector;
setListSelector(cleanedSelector);
notify(
`info`,
t(
"browser_window.attribute_modal.notifications.group_select_success",
{
count: highlighterData.groupInfo.groupSize,
}
) ||
`Selected group with ${highlighterData.groupInfo.groupSize} similar elements`
);
setCurrentListId(Date.now());
setFields({});
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: cleanedSelector });
return;
}
if (getList === true && listSelector && currentListId) {
const options = getAttributeOptions(
highlighterData.elementInfo?.tagName || "",
highlighterData.elementInfo
);
if (options.length === 1) {
const attribute = options[0].value;
let currentSelector = highlighterData.selector;
const data =
attribute === "href"
? highlighterData.elementInfo?.url || ""
: attribute === "src"
? highlighterData.elementInfo?.imageUrl || ""
: highlighterData.elementInfo?.innerText || "";
const newField: TextStep = {
id: Date.now(),
type: "text",
label: `Label ${Object.keys(fields).length + 1}`,
data: data,
selectorObj: {
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute,
},
};
const updatedFields = {
...fields,
[newField.id]: newField,
};
setFields(updatedFields);
if (listSelector) {
addListStep(
listSelector,
updatedFields,
currentListId,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: "", selector: paginationSelector }
);
}
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
}
return;
}
if (getText === true) {
const options = getAttributeOptions(
highlighterData.elementInfo?.tagName || "",
highlighterData.elementInfo
);
if (options.length === 1) {
const attribute = options[0].value;
const data =
attribute === "href"
? highlighterData.elementInfo?.url || ""
: attribute === "src"
? highlighterData.elementInfo?.imageUrl || ""
: highlighterData.elementInfo?.innerText || "";
addTextStep(
"",
data,
{
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute,
},
currentTextActionId || `text-${crypto.randomUUID()}`
);
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
}
}
},
[
getText,
getList,
listSelector,
paginationMode,
paginationType,
limitMode,
fields,
currentListId,
currentTextActionId,
currentListActionId,
addTextStep,
addListStep,
notify,
socket,
t,
paginationSelector,
]
);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (highlighterData) {
let shouldProcessClick = false;
if (!isDOMMode && canvasRef?.current) {
const canvasRect = canvasRef.current.getBoundingClientRect();
const clickX = e.clientX - canvasRect.left;
const clickY = e.clientY - canvasRect.top;
const highlightRect = highlighterData.rect;
const mappedRect =
coordinateMapper.mapBrowserRectToCanvas(highlightRect);
shouldProcessClick =
clickX >= mappedRect.left &&
clickX <= mappedRect.right &&
clickY >= mappedRect.top &&
clickY <= mappedRect.bottom;
} else {
shouldProcessClick = true;
}
if (shouldProcessClick) {
const options = getAttributeOptions( const options = getAttributeOptions(
highlighterData.elementInfo?.tagName || "", highlighterData.elementInfo?.tagName || "",
highlighterData.elementInfo highlighterData.elementInfo
@@ -742,47 +957,24 @@ export const BrowserWindow = () => {
if (getList === true && !listSelector) { if (getList === true && !listSelector) {
let cleanedSelector = highlighterData.selector; let cleanedSelector = highlighterData.selector;
if (cleanedSelector.includes("nth-child")) { if (
cleanedSelector = cleanedSelector.replace(/:nth-child\(\d+\)/g, ""); cleanedSelector.includes("[") &&
cleanedSelector.match(/\[\d+\]/)
) {
cleanedSelector = cleanedSelector.replace(/\[\d+\]/g, "");
} }
setListSelector(cleanedSelector); setListSelector(cleanedSelector);
notify( notify(
`info`, `info`,
t("browser_window.attribute_modal.notifications.list_select_success") t(
"browser_window.attribute_modal.notifications.list_select_success"
)
); );
setCurrentListId(Date.now()); setCurrentListId(Date.now());
setFields({}); setFields({});
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: cleanedSelector });
} else if (getList === true && listSelector && currentListId) { } else if (getList === true && listSelector && currentListId) {
if (options.length === 1) {
const attribute = options[0].value; const attribute = options[0].value;
let currentSelector = highlighterData.selector;
if (currentSelector.includes(">")) {
const [firstPart, ...restParts] = currentSelector
.split(">")
.map((p) => p.trim());
const listSelectorRightPart = listSelector
.split(">")
.pop()
?.trim()
.replace(/:nth-child\(\d+\)/g, "");
if (
firstPart.includes("nth-child") &&
firstPart.replace(/:nth-child\(\d+\)/g, "") ===
listSelectorRightPart
) {
currentSelector = `${firstPart.replace(
/:nth-child\(\d+\)/g,
""
)} > ${restParts.join(" > ")}`;
}
}
const data = const data =
attribute === "href" attribute === "href"
? highlighterData.elementInfo?.url || "" ? highlighterData.elementInfo?.url || ""
@@ -790,6 +982,22 @@ export const BrowserWindow = () => {
? highlighterData.elementInfo?.imageUrl || "" ? highlighterData.elementInfo?.imageUrl || ""
: highlighterData.elementInfo?.innerText || ""; : highlighterData.elementInfo?.innerText || "";
if (options.length === 1) {
let currentSelector = highlighterData.selector;
if (currentSelector.includes("/")) {
const xpathParts = currentSelector
.split("/")
.filter((part) => part);
const cleanedParts = xpathParts.map((part) => {
return part.replace(/\[\d+\]/g, "");
});
if (cleanedParts.length > 0) {
currentSelector = "//" + cleanedParts.join("/");
}
}
const newField: TextStep = { const newField: TextStep = {
id: Date.now(), id: Date.now(),
type: "text", type: "text",
@@ -828,150 +1036,6 @@ export const BrowserWindow = () => {
setShowAttributeModal(true); setShowAttributeModal(true);
} }
} }
},
[
getText,
getList,
listSelector,
paginationMode,
paginationType,
fields,
currentListId,
currentTextActionId,
currentListActionId,
addTextStep,
addListStep,
notify,
socket,
t,
paginationSelector,
]
);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (highlighterData && canvasRef?.current) {
const canvasRect = canvasRef.current.getBoundingClientRect();
const clickX = e.clientX - canvasRect.left;
const clickY = e.clientY - canvasRect.top;
const highlightRect = highlighterData.rect;
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(highlightRect);
if (
clickX >= mappedRect.left &&
clickX <= mappedRect.right &&
clickY >= mappedRect.top &&
clickY <= mappedRect.bottom
) {
const options = getAttributeOptions(highlighterData.elementInfo?.tagName || '', highlighterData.elementInfo);
if (getText === true) {
if (options.length === 1) {
// Directly use the available attribute if only one option is present
const attribute = options[0].value;
const data = attribute === 'href' ? highlighterData.elementInfo?.url || '' :
attribute === 'src' ? highlighterData.elementInfo?.imageUrl || '' :
highlighterData.elementInfo?.innerText || '';
addTextStep('', data, {
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute,
}, currentTextActionId || `text-${crypto.randomUUID()}`);
} else {
// Show the modal if there are multiple options
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
}
}
if (paginationMode && getList) {
// Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp'
if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') {
setPaginationSelector(highlighterData.selector);
notify(`info`, t('browser_window.attribute_modal.notifications.pagination_select_success'));
addListStep(listSelector!, fields, currentListId || 0, currentListActionId || `list-${crypto.randomUUID()}`, { type: paginationType, selector: highlighterData.selector });
socket?.emit('setPaginationMode', { pagination: false });
}
return;
}
if (getList === true && !listSelector) {
let cleanedSelector = highlighterData.selector;
if (cleanedSelector.includes('nth-child')) {
cleanedSelector = cleanedSelector.replace(/:nth-child\(\d+\)/g, '');
}
setListSelector(cleanedSelector);
notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success'));
setCurrentListId(Date.now());
setFields({});
} else if (getList === true && listSelector && currentListId) {
const attribute = options[0].value;
const data = attribute === 'href' ? highlighterData.elementInfo?.url || '' :
attribute === 'src' ? highlighterData.elementInfo?.imageUrl || '' :
highlighterData.elementInfo?.innerText || '';
// Add fields to the list
if (options.length === 1) {
const attribute = options[0].value;
let currentSelector = highlighterData.selector;
if (currentSelector.includes('>')) {
const [firstPart, ...restParts] = currentSelector.split('>').map(p => p.trim());
const listSelectorRightPart = listSelector.split('>').pop()?.trim().replace(/:nth-child\(\d+\)/g, '');
if (firstPart.includes('nth-child') &&
firstPart.replace(/:nth-child\(\d+\)/g, '') === listSelectorRightPart) {
currentSelector = `${firstPart.replace(/:nth-child\(\d+\)/g, '')} > ${restParts.join(' > ')}`;
}
}
const newField: TextStep = {
id: Date.now(),
type: 'text',
label: `Label ${Object.keys(fields).length + 1}`,
data: data,
selectorObj: {
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute
}
};
const updatedFields = {
...fields,
[newField.id]: newField
};
setFields(updatedFields);
if (listSelector) {
addListStep(
listSelector,
updatedFields,
currentListId,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: '', selector: paginationSelector }
);
}
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo
});
setShowAttributeModal(true);
}
}
} }
} }
}; };
@@ -1150,6 +1214,10 @@ export const BrowserWindow = () => {
{isDOMMode && highlighterData && ( {isDOMMode && highlighterData && (
<> <>
{/* Individual element highlight (for non-group or hovered element) */}
{(!getList ||
listSelector ||
!currentGroupInfo?.isGroupElement) && (
<div <div
style={{ style={{
position: "absolute", position: "absolute",
@@ -1172,6 +1240,59 @@ export const BrowserWindow = () => {
transition: "all 0.1s ease-out", transition: "all 0.1s ease-out",
}} }}
/> />
)}
{/* Group elements highlighting with real-time coordinates */}
{getList &&
!listSelector &&
currentGroupInfo?.isGroupElement &&
highlighterData.groupElements &&
highlighterData.groupElements.map((groupElement, index) => (
<React.Fragment key={index}>
{/* Highlight box */}
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y),
width: Math.min(
groupElement.rect.width,
dimensions.width
),
height: Math.min(
groupElement.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px dashed #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y - 20),
background: "#ff00c3",
color: "white",
padding: "2px 6px",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "2px",
pointerEvents: "none",
zIndex: 1001,
whiteSpace: "nowrap",
}}
>
List item {index + 1}
</div>
</React.Fragment>
))}
</> </>
)} )}
</> </>
@@ -1186,6 +1307,7 @@ export const BrowserWindow = () => {
getList={getList} getList={getList}
getText={getText} getText={getText}
listSelector={listSelector} listSelector={listSelector}
cachedChildSelectors={cachedChildSelectors}
paginationMode={paginationMode} paginationMode={paginationMode}
paginationType={paginationType} paginationType={paginationType}
limitMode={limitMode} limitMode={limitMode}