Files
parcer/src/components/browser/BrowserWindow.tsx

1578 lines
55 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
2024-06-14 21:47:42 +05:30
import { useSocketStore } from '../../context/socket';
2024-08-21 21:57:41 +05:30
import { Button } from '@mui/material';
2025-06-27 04:31:20 +05:30
import Canvas from "../recorder/Canvas";
2025-01-09 20:07:42 +05:30
import { Highlighter } from "../recorder/Highlighter";
2025-01-09 19:49:20 +05:30
import { GenericModal } from '../ui/GenericModal';
2024-07-24 20:54:13 +05:30
import { useActionContext } from '../../context/browserActions';
2024-08-09 06:23:10 +05:30
import { useBrowserSteps, TextStep } from '../../context/browserSteps';
2024-10-28 06:38:24 +05:30
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next';
2025-03-08 17:10:30 +05:30
import { AuthContext } from '../../context/auth';
2025-03-14 12:35:35 +05:30
import { coordinateMapper } from '../../helpers/coordinateMapper';
import { useBrowserDimensionsStore } from '../../context/browserDimensions';
import { clientSelectorGenerator, ElementFingerprint } from "../../helpers/clientSelectorGenerator";
import DatePicker from "../pickers/DatePicker";
import Dropdown from "../pickers/Dropdown";
import TimePicker from "../pickers/TimePicker";
import DateTimeLocalPicker from "../pickers/DateTimeLocalPicker";
import { DOMBrowserRenderer } from '../recorder/DOMBrowserRenderer';
2024-07-27 02:06:05 +05:30
interface ElementInfo {
tagName: string;
hasOnlyText?: boolean;
isIframeContent?: boolean;
isShadowRoot?: boolean;
2024-07-27 02:06:05 +05:30
innerText?: string;
url?: string;
imageUrl?: string;
2024-10-04 22:33:40 +05:30
attributes?: Record<string, string>;
innerHTML?: string;
outerHTML?: string;
isDOMMode?: boolean;
2024-07-27 02:11:59 +05:30
}
2024-08-04 02:54:06 +05:30
interface AttributeOption {
label: string;
value: string;
}
2025-03-08 17:10:30 +05:30
interface ScreencastData {
image: string;
userId: string;
2025-03-14 12:35:35 +05:30
viewport?: ViewportInfo | null;
}
interface ViewportInfo {
width: number;
height: number;
2025-03-08 17:10:30 +05:30
}
interface RRWebSnapshot {
type: number;
childNodes?: RRWebSnapshot[];
tagName?: string;
attributes?: Record<string, string>;
textContent: string;
id: number;
[key: string]: any;
}
interface ProcessedSnapshot {
snapshot: RRWebSnapshot;
resources: {
stylesheets: Array<{
href: string;
content: string;
media?: string;
}>;
images: Array<{
src: string;
dataUrl: string;
alt?: string;
}>;
fonts: Array<{
url: string;
dataUrl: string;
format?: string;
}>;
scripts: Array<{
src: string;
content: string;
type?: string;
}>;
media: Array<{
src: string;
dataUrl: string;
type: string;
}>;
};
baseUrl: string;
viewport: { width: number; height: number };
timestamp: number;
processingStats: {
totalReplacements: number;
discoveredResources: {
images: number;
stylesheets: number;
scripts: number;
fonts: number;
media: number;
};
cachedResources: {
stylesheets: number;
images: number;
fonts: number;
scripts: number;
media: number;
};
totalCacheSize: number;
};
}
interface RRWebDOMCastData {
snapshotData: ProcessedSnapshot;
userId: string;
timestamp: number;
}
2025-03-08 17:10:30 +05:30
const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): AttributeOption[] => {
if (!elementInfo) return [];
2024-08-04 02:54:06 +05:30
switch (tagName.toLowerCase()) {
case 'a':
const anchorOptions: AttributeOption[] = [];
if (elementInfo.innerText) {
anchorOptions.push({ label: `Text: ${elementInfo.innerText}`, value: 'innerText' });
}
if (elementInfo.url) {
anchorOptions.push({ label: `URL: ${elementInfo.url}`, value: 'href' });
}
return anchorOptions;
2024-08-04 02:54:06 +05:30
case 'img':
const imgOptions: AttributeOption[] = [];
if (elementInfo.innerText) {
imgOptions.push({ label: `Alt Text: ${elementInfo.innerText}`, value: 'alt' });
}
if (elementInfo.imageUrl) {
imgOptions.push({ label: `Image URL: ${elementInfo.imageUrl}`, value: 'src' });
}
return imgOptions;
2024-08-04 02:54:06 +05:30
default:
2024-08-21 22:51:10 +05:30
return [{ label: `Text: ${elementInfo.innerText}`, value: 'innerText' }];
2024-08-04 02:54:06 +05:30
}
};
2024-06-14 21:47:42 +05:30
export const BrowserWindow = () => {
const { t } = useTranslation();
const { browserWidth, browserHeight } = useBrowserDimensionsStore();
2024-06-14 21:47:42 +05:30
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{
rect: DOMRect;
selector: string;
elementInfo: ElementInfo | null;
isShadow?: boolean;
childSelectors?: string[];
groupElements?: Array<{ element: HTMLElement; rect: DOMRect }>;
similarElements?: {
elements: HTMLElement[];
rects: DOMRect[];
};
} | null>(null);
2024-08-04 03:18:41 +05:30
const [showAttributeModal, setShowAttributeModal] = useState(false);
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
2024-08-04 03:23:02 +05:30
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
const [currentListId, setCurrentListId] = useState<number | null>(null);
const [viewportInfo, setViewportInfo] = useState<ViewportInfo>({ width: browserWidth, height: browserHeight });
const [isLoading, setIsLoading] = useState(false);
const [cachedChildSelectors, setCachedChildSelectors] = useState<string[]>([]);
2024-08-09 06:23:10 +05:30
const [listSelector, setListSelector] = useState<string | null>(null);
const [fields, setFields] = useState<Record<string, TextStep>>({});
2024-09-06 21:04:24 +05:30
const [paginationSelector, setPaginationSelector] = useState<string>('');
2024-09-06 22:55:54 +05:30
const highlighterUpdateRef = useRef<number>(0);
const [isCachingChildSelectors, setIsCachingChildSelectors] = useState(false);
const [cachedListSelector, setCachedListSelector] = useState<string | null>(
null
);
const [pendingNotification, setPendingNotification] = useState<{
type: "error" | "warning" | "info" | "success";
message: string;
count?: number;
} | null>(null);
2024-06-14 21:47:42 +05:30
const { socket } = useSocketStore();
2025-07-06 21:43:28 +05:30
const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode, currentSnapshot } = useGlobalInfoStore();
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
const { addTextStep, addListStep } = useBrowserSteps();
const [currentGroupInfo, setCurrentGroupInfo] = useState<{
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
} | null>(null);
2025-03-11 21:31:57 +05:30
2025-03-08 17:10:30 +05:30
const { state } = useContext(AuthContext);
2025-03-11 21:31:57 +05:30
const { user } = state;
2024-06-14 21:47:42 +05:30
const [datePickerInfo, setDatePickerInfo] = useState<{
coordinates: { x: number; y: number };
selector: string;
} | null>(null);
const [dropdownInfo, setDropdownInfo] = useState<{
coordinates: { x: number; y: number };
selector: string;
options: Array<{
value: string;
text: string;
disabled: boolean;
selected: boolean;
}>;
} | null>(null);
const [timePickerInfo, setTimePickerInfo] = useState<{
coordinates: { x: number; y: number };
selector: string;
} | null>(null);
const [dateTimeLocalInfo, setDateTimeLocalInfo] = useState<{
coordinates: { x: number; y: number };
selector: string;
} | null>(null);
const dimensions = {
width: browserWidth,
height: browserHeight
};
2025-03-15 14:14:43 +05:30
const handleShowDatePicker = useCallback(
(info: { coordinates: { x: number; y: number }; selector: string }) => {
setDatePickerInfo(info);
},
[]
);
const handleShowDropdown = useCallback(
(info: {
coordinates: { x: number; y: number };
selector: string;
options: Array<{
value: string;
text: string;
disabled: boolean;
selected: boolean;
}>;
}) => {
setDropdownInfo(info);
},
[]
);
const handleShowTimePicker = useCallback(
(info: { coordinates: { x: number; y: number }; selector: string }) => {
setTimePickerInfo(info);
},
[]
);
const handleShowDateTimePicker = useCallback(
(info: { coordinates: { x: number; y: number }; selector: string }) => {
setDateTimeLocalInfo(info);
},
[]
);
const rrwebSnapshotHandler = useCallback(
(data: RRWebDOMCastData) => {
if (!data.userId || data.userId === user?.id) {
if (data.snapshotData && data.snapshotData.snapshot) {
2025-07-06 21:43:28 +05:30
updateDOMMode(true, data.snapshotData);
socket?.emit("dom-mode-enabled");
setIsLoading(false);
} else {
setIsLoading(false);
}
}
},
2025-07-06 21:43:28 +05:30
[user?.id, socket, updateDOMMode]
);
const domModeHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
2025-07-06 21:43:28 +05:30
updateDOMMode(true);
socket?.emit("dom-mode-enabled");
setIsLoading(false);
}
},
2025-07-06 21:43:28 +05:30
[user?.id, socket, updateDOMMode]
);
const domModeErrorHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
2025-07-06 21:43:28 +05:30
updateDOMMode(false);
setIsLoading(false);
}
},
2025-07-06 21:43:28 +05:30
[user?.id, updateDOMMode]
);
useEffect(() => {
if (isDOMMode) {
clientSelectorGenerator.setGetList(getList);
clientSelectorGenerator.setListSelector(listSelector || "");
clientSelectorGenerator.setPaginationMode(paginationMode);
}
}, [isDOMMode, getList, listSelector, paginationMode]);
useEffect(() => {
if (isDOMMode && listSelector) {
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: listSelector });
clientSelectorGenerator.setListSelector(listSelector);
if (currentSnapshot && cachedListSelector !== listSelector) {
setCachedChildSelectors([]);
setIsCachingChildSelectors(true);
setCachedListSelector(listSelector);
const iframeElement = document.querySelector(
"#dom-browser-iframe"
) as HTMLIFrameElement;
if (iframeElement?.contentDocument) {
setTimeout(() => {
try {
const childSelectors =
clientSelectorGenerator.getChildSelectors(
iframeElement.contentDocument as Document,
listSelector
);
clientSelectorGenerator.precomputeChildSelectorMappings(
childSelectors,
iframeElement.contentDocument as Document
);
setCachedChildSelectors(childSelectors);
} catch (error) {
console.error("Error during child selector caching:", error);
} finally {
setIsCachingChildSelectors(false);
if (pendingNotification) {
notify(pendingNotification.type, pendingNotification.message);
setPendingNotification(null);
}
}
}, 100);
} else {
setIsCachingChildSelectors(false);
}
}
}
}, [
isDOMMode,
listSelector,
socket,
getList,
currentSnapshot,
cachedListSelector,
pendingNotification,
notify,
]);
useEffect(() => {
if (!listSelector) {
setCachedListSelector(null);
}
}, [listSelector]);
2025-03-14 12:35:35 +05:30
useEffect(() => {
2025-03-15 14:14:43 +05:30
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
}, [viewportInfo, dimensions.width, dimensions.height]);
2025-03-14 12:35:35 +05:30
2025-03-11 12:58:36 +05:30
useEffect(() => {
if (listSelector) {
sessionStorage.setItem('recordingListSelector', listSelector);
2025-03-11 12:58:36 +05:30
}
}, [listSelector]);
useEffect(() => {
const storedListSelector = sessionStorage.getItem('recordingListSelector');
2025-03-11 12:58:36 +05:30
// Only restore state if it exists in sessionStorage
if (storedListSelector && !listSelector) {
setListSelector(storedListSelector);
}
2025-03-11 21:31:57 +05:30
}, []);
2025-03-11 12:58:36 +05:30
2024-07-23 21:58:28 +05:30
const onMouseMove = (e: MouseEvent) => {
if (canvasRef && canvasRef.current && highlighterData) {
const canvasRect = canvasRef.current.getBoundingClientRect();
// mousemove outside the browser window
2024-07-23 21:58:28 +05:30
if (
e.pageX < canvasRect.left
|| e.pageX > canvasRect.right
|| e.pageY < canvasRect.top
|| e.pageY > canvasRect.bottom
) {
setHighlighterData(null);
}
}
};
const resetListState = useCallback(() => {
setListSelector(null);
setFields({});
setCurrentListId(null);
setCachedChildSelectors([]);
2024-10-27 19:25:10 +05:30
}, []);
useEffect(() => {
if (!getList) {
resetListState();
}
}, [getList, resetListState]);
2025-03-08 17:10:30 +05:30
const screencastHandler = useCallback((data: string | ScreencastData) => {
if (typeof data === 'string') {
setScreenShot(data);
} else if (data && typeof data === 'object' && 'image' in data) {
if (!data.userId || data.userId === user?.id) {
setScreenShot(data.image);
2025-03-14 12:35:35 +05:30
if (data.viewport) {
setViewportInfo(data.viewport);
}
2025-03-08 17:10:30 +05:30
}
}
}, [screenShot, user?.id]);
2024-06-14 21:47:42 +05:30
2024-06-14 23:17:32 +05:30
useEffect(() => {
2024-06-14 21:47:42 +05:30
if (socket) {
socket.on("screencast", screencastHandler);
socket.on("domcast", rrwebSnapshotHandler);
socket.on("dom-mode-enabled", domModeHandler);
socket.on("dom-mode-error", domModeErrorHandler);
2024-06-14 21:47:42 +05:30
}
if (canvasRef?.current && !isDOMMode && screenShot) {
2024-06-14 21:47:42 +05:30
drawImage(screenShot, canvasRef.current);
}
2024-06-14 21:47:42 +05:30
return () => {
if (socket) {
socket.off("screencast", screencastHandler);
socket.off("domcast", rrwebSnapshotHandler);
socket.off("dom-mode-enabled", domModeHandler);
socket.off("dom-mode-error", domModeErrorHandler);
}
};
}, [
socket,
screenShot,
canvasRef,
isDOMMode,
screencastHandler,
rrwebSnapshotHandler,
domModeHandler,
domModeErrorHandler,
]);
const domHighlighterHandler = useCallback(
(data: {
rect: DOMRect;
selector: string;
elementInfo: ElementInfo | null;
childSelectors?: string[];
isShadow?: boolean;
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
groupFingerprint: ElementFingerprint;
};
similarElements?: {
elements: HTMLElement[];
rects: DOMRect[];
};
isDOMMode?: boolean;
}) => {
if (!getText && !getList) {
setHighlighterData(null);
return;
}
if (!isDOMMode || !currentSnapshot) {
return;
}
let iframeElement = document.querySelector(
"#dom-browser-iframe"
) as HTMLIFrameElement;
if (!iframeElement) {
iframeElement = document.querySelector(
"#browser-window iframe"
) as HTMLIFrameElement;
}
if (!iframeElement) {
console.error("Could not find iframe element for DOM highlighting");
return;
}
const iframeRect = iframeElement.getBoundingClientRect();
const IFRAME_BODY_PADDING = 16;
let mappedSimilarElements;
if (data.similarElements) {
mappedSimilarElements = {
elements: data.similarElements.elements,
rects: data.similarElements.rects.map(
(rect) =>
new DOMRect(
rect.x + iframeRect.left - IFRAME_BODY_PADDING,
rect.y + iframeRect.top - IFRAME_BODY_PADDING,
rect.width,
rect.height
)
),
};
}
if (data.groupInfo) {
setCurrentGroupInfo(data.groupInfo);
} else {
setCurrentGroupInfo(null);
}
const absoluteRect = new DOMRect(
data.rect.x + iframeRect.left - IFRAME_BODY_PADDING,
data.rect.y + iframeRect.top - IFRAME_BODY_PADDING,
data.rect.width,
data.rect.height
);
const mappedData = {
...data,
rect: absoluteRect,
childSelectors: data.childSelectors || cachedChildSelectors,
2025-07-16 01:07:39 +05:30
similarElements: mappedSimilarElements,
};
if (getList === true) {
if (!listSelector && data.groupInfo?.isGroupElement) {
const updatedGroupElements = data.groupInfo.groupElements.map(
(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) &&
mappedData.childSelectors.length > 0;
if (limitMode) {
setHighlighterData(null);
} else if (paginationMode) {
if (
paginationType !== "" &&
!["none", "scrollDown", "scrollUp"].includes(paginationType)
) {
setHighlighterData(mappedData);
} else {
setHighlighterData(null);
}
} else if (hasChildSelectors) {
setHighlighterData(mappedData);
} else {
setHighlighterData(null);
}
} else {
setHighlighterData(mappedData);
}
} else {
setHighlighterData(mappedData);
}
},
[
isDOMMode,
currentSnapshot,
getText,
getList,
socket,
listSelector,
paginationMode,
paginationType,
limitMode,
cachedChildSelectors,
]
);
2024-06-14 21:47:42 +05:30
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();
if (now - highlighterUpdateRef.current < 16) {
return;
}
highlighterUpdateRef.current = now;
2025-03-14 12:35:35 +05:30
// Map the incoming DOMRect from browser coordinates to canvas coordinates
const mappedRect = new DOMRect(
data.rect.x,
data.rect.y,
data.rect.width,
data.rect.height
);
const mappedData = {
...data,
rect: mappedRect
};
2024-09-06 11:06:44 +05:30
if (getList === true) {
if (listSelector) {
socket?.emit('listSelector', { selector: listSelector });
2025-03-14 12:35:35 +05:30
const hasValidChildSelectors = Array.isArray(mappedData.childSelectors) && mappedData.childSelectors.length > 0;
if (limitMode) {
setHighlighterData(null);
} else if (paginationMode) {
// Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
2025-03-14 12:35:35 +05:30
setHighlighterData(mappedData);
} else {
setHighlighterData(null);
}
2025-03-14 12:35:35 +05:30
} else if (mappedData.childSelectors && mappedData.childSelectors.includes(mappedData.selector)) {
// Highlight only valid child elements within the listSelector
2025-03-14 12:35:35 +05:30
setHighlighterData(mappedData);
} else if (mappedData.elementInfo?.isIframeContent && mappedData.childSelectors) {
// Handle iframe elements
const isIframeChild = mappedData.childSelectors.some(childSelector =>
mappedData.selector.includes(':>>') &&
2025-01-09 17:15:08 +05:30
childSelector.split(':>>').some(part =>
2025-03-14 12:35:35 +05:30
mappedData.selector.includes(part.trim())
)
);
2025-03-14 12:35:35 +05:30
setHighlighterData(isIframeChild ? mappedData : null);
} else if (mappedData.selector.includes(':>>') && hasValidChildSelectors) {
// Handle mixed DOM cases with iframes
2025-03-14 12:35:35 +05:30
const selectorParts = mappedData.selector.split(':>>').map(part => part.trim());
2025-01-09 17:15:08 +05:30
const isValidMixedSelector = selectorParts.some(part =>
2025-03-14 12:35:35 +05:30
mappedData.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
2025-03-14 12:35:35 +05:30
setHighlighterData(isValidMixedSelector ? mappedData : null);
} else if (mappedData.elementInfo?.isShadowRoot && mappedData.childSelectors) {
// Handle Shadow DOM elements
const isShadowChild = mappedData.childSelectors.some(childSelector =>
mappedData.selector.includes('>>') &&
2025-01-09 17:15:08 +05:30
childSelector.split('>>').some(part =>
2025-03-14 12:35:35 +05:30
mappedData.selector.includes(part.trim())
)
);
2025-03-14 12:35:35 +05:30
setHighlighterData(isShadowChild ? mappedData : null);
} else if (mappedData.selector.includes('>>') && hasValidChildSelectors) {
// Handle mixed DOM cases
const selectorParts = mappedData.selector.split('>>').map(part => part.trim());
2025-01-09 17:15:08 +05:30
const isValidMixedSelector = selectorParts.some(part =>
2025-03-14 12:35:35 +05:30
mappedData.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
2025-03-14 12:35:35 +05:30
setHighlighterData(isValidMixedSelector ? mappedData : null);
2025-01-09 17:15:08 +05:30
} else {
2025-03-14 12:35:35 +05:30
// If not a valid child in normal mode, clear the highlighter
2024-09-06 11:06:44 +05:30
setHighlighterData(null);
2025-01-09 17:15:08 +05:30
}
} else {
// Set highlighterData for the initial listSelector selection
2025-03-14 12:35:35 +05:30
setHighlighterData(mappedData);
2025-01-09 17:15:08 +05:30
}
} else {
// For non-list steps
2025-03-14 12:35:35 +05:30
setHighlighterData(mappedData);
2025-01-09 17:15:08 +05:30
}
2025-03-14 12:35:35 +05:30
}, [getList, socket, listSelector, paginationMode, paginationType, limitMode]);
2024-09-06 11:04:59 +05:30
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]);
2025-03-11 12:58:36 +05:30
useEffect(() => {
if (socket && listSelector) {
socket.emit('setGetList', { getList: true });
socket.emit('listSelector', { selector: listSelector });
}
}, [socket, listSelector]);
2024-06-25 22:39:29 +05:30
useEffect(() => {
if (captureStage === 'initial' && listSelector) {
socket?.emit('setGetList', { getList: true });
socket?.emit('listSelector', { selector: listSelector });
}
}, [captureStage, listSelector, socket]);
const handleDOMElementSelection = useCallback(
(highlighterData: {
rect: DOMRect;
selector: string;
isShadow?: boolean;
elementInfo: ElementInfo | null;
childSelectors?: string[];
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
};
}) => {
setShowAttributeModal(false);
setSelectedElement(null);
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,
isShadow: highlighterData.isShadow
},
undefined,
highlighterData.isShadow
);
socket?.emit("setPaginationMode", { pagination: false });
}
return;
}
if (
getList === true &&
!listSelector &&
highlighterData.groupInfo?.isGroupElement
) {
let cleanedSelector = highlighterData.selector;
setListSelector(cleanedSelector);
notify(
`info`,
t(
2025-07-06 16:49:20 +05:30
"browser_window.attribute_modal.notifications.list_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,
2025-07-16 01:07:39 +05:30
isShadow: highlighterData.isShadow || 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 },
undefined,
highlighterData.isShadow
);
}
} 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,
isShadow: highlighterData.isShadow || 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(
highlighterData.elementInfo?.tagName || "",
highlighterData.elementInfo
);
if (getText === true) {
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,
isShadow: highlighterData.isShadow || highlighterData.elementInfo?.isShadowRoot,
attribute,
},
currentTextActionId || `text-${crypto.randomUUID()}`
);
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
}
}
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, isShadow: highlighterData.isShadow },
undefined,
highlighterData.isShadow
);
socket?.emit("setPaginationMode", { pagination: false });
}
return;
}
if (getList === true && !listSelector) {
let cleanedSelector = highlighterData.selector;
if (
cleanedSelector.includes("[") &&
cleanedSelector.match(/\[\d+\]/)
) {
cleanedSelector = cleanedSelector.replace(/\[\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 || "";
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 = {
id: Date.now(),
type: "text",
label: `Label ${Object.keys(fields).length + 1}`,
data: data,
selectorObj: {
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
2025-07-16 01:07:39 +05:30
isShadow: highlighterData.isShadow || 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, isShadow: highlighterData.isShadow },
undefined,
highlighterData.isShadow
);
}
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
2024-10-27 19:25:10 +05:30
}
}
2024-08-04 03:18:41 +05:30
}
}
2024-08-04 03:18:41 +05:30
};
2024-08-21 23:38:33 +05:30
2024-08-04 03:18:41 +05:30
const handleAttributeSelection = (attribute: string) => {
if (selectedElement) {
let data = '';
switch (attribute) {
case 'href':
data = selectedElement.info?.url || '';
break;
case 'src':
data = selectedElement.info?.imageUrl || '';
break;
default:
data = selectedElement.info?.innerText || '';
}
2024-08-06 02:16:53 +05:30
{
2024-08-09 06:23:10 +05:30
if (getText === true) {
2024-08-06 03:11:28 +05:30
addTextStep('', data, {
2024-08-06 02:16:53 +05:30
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
isShadow: highlighterData?.isShadow || selectedElement.info?.isShadowRoot,
2024-08-06 02:16:53 +05:30
attribute: attribute
2025-05-26 21:41:40 +05:30
}, currentTextActionId || `text-${crypto.randomUUID()}`);
2024-08-06 02:16:53 +05:30
}
2024-09-07 22:20:53 +05:30
if (getList === true && listSelector && currentListId) {
const newField: TextStep = {
id: Date.now(),
type: 'text',
label: `Label ${Object.keys(fields).length + 1}`,
data: data,
selectorObj: {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
2025-07-16 01:07:39 +05:30
isShadow: highlighterData?.isShadow || highlighterData?.elementInfo?.isShadowRoot,
attribute: attribute
}
};
2025-05-07 09:18:08 +05:30
const updatedFields = {
...fields,
[newField.id]: newField
};
setFields(updatedFields);
2024-09-21 15:56:51 +05:30
if (listSelector) {
2025-05-07 09:18:08 +05:30
addListStep(
listSelector,
updatedFields,
currentListId,
2025-05-26 21:41:40 +05:30
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: "", selector: paginationSelector, isShadow: highlighterData?.isShadow },
undefined,
highlighterData?.isShadow
2025-05-07 09:18:08 +05:30
);
2024-09-21 15:56:51 +05:30
}
}
2024-08-06 02:16:53 +05:30
}
}
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
setTimeout(() => {
2024-08-04 03:18:41 +05:30
setShowAttributeModal(false);
}, 0);
};
2024-09-06 21:08:22 +05:30
const resetPaginationSelector = useCallback(() => {
setPaginationSelector('');
}, []);
2024-09-06 22:07:00 +05:30
2024-09-06 21:08:22 +05:30
useEffect(() => {
if (!paginationMode) {
resetPaginationSelector();
}
}, [paginationMode, resetPaginationSelector]);
2024-06-14 21:47:42 +05:30
return (
<div
onClick={handleClick}
style={{ width: browserWidth }}
id="browser-window"
>
{/* Attribute selection modal */}
{(getText === true || getList === true) && (
<GenericModal
isOpen={showAttributeModal}
onClose={() => {
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
}}
canBeClosed={true}
modalStyle={modalStyle}
>
<div>
<h2>Select Attribute</h2>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "20px",
marginTop: "30px",
}}
>
{attributeOptions.map((option) => (
<Button
variant="outlined"
size="medium"
key={option.value}
onClick={() => {
handleAttributeSelection(option.value);
}}
style={{
justifyContent: "flex-start",
maxWidth: "80%",
overflow: "hidden",
}}
sx={{
color: "#ff00c3 !important",
borderColor: "#ff00c3 !important",
backgroundColor: "whitesmoke !important",
}}
>
<span
style={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
}}
2024-08-21 22:32:20 +05:30
>
{option.label}
</span>
</Button>
))}
</div>
</div>
</GenericModal>
)}
{datePickerInfo && (
<DatePicker
coordinates={datePickerInfo.coordinates}
selector={datePickerInfo.selector}
onClose={() => setDatePickerInfo(null)}
/>
)}
{dropdownInfo && (
<Dropdown
coordinates={dropdownInfo.coordinates}
selector={dropdownInfo.selector}
options={dropdownInfo.options}
onClose={() => setDropdownInfo(null)}
/>
)}
{timePickerInfo && (
<TimePicker
coordinates={timePickerInfo.coordinates}
selector={timePickerInfo.selector}
onClose={() => setTimePickerInfo(null)}
/>
)}
{dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={dateTimeLocalInfo.coordinates}
selector={dateTimeLocalInfo.selector}
onClose={() => setDateTimeLocalInfo(null)}
/>
)}
{/* Main content area */}
<div style={{ height: dimensions.height, overflow: "hidden" }}>
{/* Add CSS for the spinner animation */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
{(getText === true || getList === true) &&
!showAttributeModal &&
highlighterData?.rect != null && (
<>
{!isDOMMode && canvasRef?.current && (
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={dimensions.width}
height={dimensions.height}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
)}
{isDOMMode && highlighterData && (
<>
{/* Individual element highlight (for non-group or hovered element) */}
{((getText && !listSelector) ||
(getList && paginationMode && paginationType !== "" &&
!["none", "scrollDown", "scrollUp"].includes(paginationType))) && (
<div
style={{
position: "absolute",
left: Math.max(0, highlighterData.rect.x),
top: Math.max(0, highlighterData.rect.y),
width: Math.min(
highlighterData.rect.width,
dimensions.width
),
height: Math.min(
highlighterData.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px solid #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
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>
)
)}
{getList &&
listSelector &&
!paginationMode &&
!limitMode &&
highlighterData?.similarElements &&
highlighterData.similarElements.rects.map(
(rect, index) => (
<React.Fragment key={`item-${index}`}>
{/* Highlight box for similar element */}
<div
style={{
position: "absolute",
left: Math.max(0, rect.x),
top: Math.max(0, rect.y),
width: Math.min(rect.width, dimensions.width),
height: Math.min(
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",
}}
/>
{/* Label for similar element */}
<div
style={{
position: "absolute",
left: Math.max(0, rect.x),
top: Math.max(0, rect.y - 20),
background: "#ff00c3",
color: "white",
padding: "2px 6px",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "2px",
pointerEvents: "none",
zIndex: 1001,
whiteSpace: "nowrap",
}}
>
Item {index + 1}
</div>
</React.Fragment>
)
)}
</>
)}
</>
)}
{isDOMMode ? (
<div
style={{ position: "relative", width: "100%", height: "100%" }}
>
{currentSnapshot ? (
<DOMBrowserRenderer
width={dimensions.width}
height={dimensions.height}
snapshot={currentSnapshot}
getList={getList}
getText={getText}
listSelector={listSelector}
cachedChildSelectors={cachedChildSelectors}
paginationMode={paginationMode}
paginationType={paginationType}
limitMode={limitMode}
onHighlight={(data) => {
domHighlighterHandler(data);
}}
isCachingChildSelectors={isCachingChildSelectors}
onElementSelect={handleDOMElementSelection}
onShowDatePicker={handleShowDatePicker}
onShowDropdown={handleShowDropdown}
onShowTimePicker={handleShowTimePicker}
onShowDateTimePicker={handleShowDateTimePicker}
/>
) : (
<div
style={{
width: dimensions.width,
height: dimensions.height,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
borderRadius: "5px",
flexDirection: "column",
gap: "20px",
}}
>
<div
style={{
width: "60px",
height: "60px",
borderTop: "4px solid transparent",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
<div
style={{
fontSize: "18px",
color: "#ff00c3",
fontWeight: "bold",
}}
>
Loading website...
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)}
{/* Loading overlay positioned specifically over DOM content */}
{isCachingChildSelectors && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(255, 255, 255, 0.8)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 9999,
pointerEvents: "none",
borderRadius: "0px 0px 5px 5px", // Match the DOM renderer border radius
}}
>
<div
style={{
width: "40px",
height: "40px",
border: "4px solid #f3f3f3",
borderTop: "4px solid #ff00c3",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
</div>
)}
2024-10-18 19:07:31 +05:30
</div>
) : (
/* Screenshot mode canvas */
<Canvas
onCreateRef={setCanvasReference}
width={dimensions.width}
height={dimensions.height}
/>
)}
2024-07-21 23:51:55 +05:30
</div>
</div>
2024-06-14 21:47:42 +05:30
);
};
2024-06-14 23:17:32 +05:30
const drawImage = (image: string, canvas: HTMLCanvasElement): void => {
2024-06-14 21:47:42 +05:30
const ctx = canvas.getContext('2d');
if (!ctx) return;
2024-06-14 21:47:42 +05:30
const img = new Image();
img.onload = () => {
requestAnimationFrame(() => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
});
if (image.startsWith('blob:')) {
URL.revokeObjectURL(image);
}
2024-06-14 21:47:42 +05:30
};
img.onerror = () => {
console.warn('Failed to load image');
};
img.src = image;
2024-10-25 00:46:44 +05:30
};
const modalStyle = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '30%',
backgroundColor: 'background.paper',
p: 4,
height: 'fit-content',
display: 'block',
padding: '20px',
};