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

1400 lines
51 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, childSelectors?: string[], groupElements?: Array<{ element: HTMLElement; rect: 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);
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();
2025-05-07 09:18:08 +05:30
const { addTextStep, addListStep, updateListStepData } = 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 screenshotModeHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
2025-07-06 21:43:28 +05:30
updateDOMMode(false);
socket?.emit("screenshot-mode-enabled");
setIsLoading(false);
}
},
2025-07-06 21:43:28 +05:30
[user?.id, 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);
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, currentSnapshot]);
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);
2025-07-06 21:43:28 +05:30
// socket.on("screenshot-mode-enabled", screenshotModeHandler);
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) {
console.log("Cleaning up DOM streaming event listeners");
socket.off("screencast", screencastHandler);
socket.off("domcast", rrwebSnapshotHandler);
socket.off("dom-mode-enabled", domModeHandler);
2025-07-06 21:43:28 +05:30
// socket.off("screenshot-mode-enabled", screenshotModeHandler);
socket.off("dom-mode-error", domModeErrorHandler);
}
};
}, [
socket,
screenShot,
canvasRef,
isDOMMode,
screencastHandler,
rrwebSnapshotHandler,
domModeHandler,
2025-07-06 21:43:28 +05:30
// screenshotModeHandler,
domModeErrorHandler,
]);
const domHighlighterHandler = useCallback(
(data: {
rect: DOMRect;
selector: string;
elementInfo: ElementInfo | null;
childSelectors?: string[];
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
groupFingerprint: ElementFingerprint;
};
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;
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,
};
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
2024-06-14 23:17:32 +05:30
useEffect(() => {
2024-07-23 21:58:28 +05:30
document.addEventListener('mousemove', onMouseMove, false);
2024-06-14 21:47:42 +05:30
if (socket) {
2025-03-11 12:58:36 +05:30
socket.off("highlighter", highlighterHandler);
socket.on("highlighter", highlighterHandler);
2024-06-14 21:47:42 +05:30
}
return () => {
2025-03-11 12:58:36 +05:30
document.removeEventListener('mousemove', onMouseMove);
if (socket) {
socket.off("highlighter", highlighterHandler);
}
2024-06-14 21:47:42 +05:30
};
2025-03-11 12:58:36 +05:30
}, [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]);
2025-03-11 12:58:36 +05:30
useEffect(() => {
if (socket && listSelector) {
console.log('Syncing list selector with server:', 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;
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 }
);
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,
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(
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,
shadow: 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 }
);
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,
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);
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,
shadow: 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,
shadow: selectedElement.info?.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()}`,
2025-05-07 09:18:08 +05:30
{ type: '', selector: paginationSelector }
);
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">
{
2024-08-21 22:32:20 +05:30
getText === true || getList === true ? (
<GenericModal
isOpen={showAttributeModal}
onClose={() => {
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
}}
canBeClosed={true}
2024-10-25 00:46:44 +05:30
modalStyle={modalStyle}
2024-08-21 22:32:20 +05:30
>
<div>
<h2>Select Attribute</h2>
2024-08-21 22:42:54 +05:30
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', marginTop: '30px' }}>
2024-08-21 22:32:20 +05:30
{attributeOptions.map((option) => (
<Button
variant="outlined"
size="medium"
key={option.value}
onClick={() => handleAttributeSelection(option.value)}
style={{
justifyContent: 'flex-start',
2024-08-21 22:37:32 +05:30
maxWidth: '80%',
2024-08-21 22:32:20 +05:30
overflow: 'hidden',
padding: '5px 10px',
}}
2025-01-09 17:15:08 +05:30
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
2025-01-09 17:15:08 +05:30
}}
2024-08-21 22:32:20 +05:30
>
<span style={{
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%'
}}>
{option.label}
</span>
</Button>
))}
</div>
</div>
</GenericModal>
) : null
}
{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)}
/>
)}
<div style={{ height: dimensions.height, overflow: "hidden" }}>
{(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) */}
{(!getList ||
listSelector ||
!currentGroupInfo?.isGroupElement) && (
<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>
))}
</>
)}
</>
)}
{isDOMMode ? (
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: any) => {
domHighlighterHandler(data);
}}
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>
)
) : (
/* Screenshot mode canvas */
<Canvas
onCreateRef={setCanvasReference}
2025-03-15 14:14:43 +05:30
width={dimensions.width}
height={dimensions.height}
2024-10-27 19:25:10 +05:30
/>
)}
2024-10-18 19:07:31 +05:30
</div>
2024-07-21 23:51:55 +05:30
</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',
};