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

1284 lines
49 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 } 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[] } | 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 [isDOMMode, setIsDOMMode] = useState(false);
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(null);
const [isLoading, setIsLoading] = useState(false);
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-05-20 17:44:00 +05:30
const { notify, currentTextActionId, currentListActionId } = useGlobalInfoStore();
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
2025-05-07 09:18:08 +05:30
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
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) {
setCurrentSnapshot(data.snapshotData);
setIsDOMMode(true);
socket?.emit("dom-mode-enabled");
setIsLoading(false);
} else {
setIsLoading(false);
}
}
},
[user?.id, socket]
);
const domModeHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
setIsDOMMode(true);
socket?.emit("dom-mode-enabled");
setIsLoading(false);
}
},
[user?.id, socket]
);
const screenshotModeHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
setIsDOMMode(false);
socket?.emit("screenshot-mode-enabled");
setCurrentSnapshot(null);
setIsLoading(false);
}
},
[user?.id]
);
const domModeErrorHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
setIsDOMMode(false);
setCurrentSnapshot(null);
setIsLoading(false);
}
},
[user?.id]
);
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);
}
}, [isDOMMode, listSelector, socket, getList]);
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);
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("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);
socket.off("screenshot-mode-enabled", screenshotModeHandler);
socket.off("dom-mode-error", domModeErrorHandler);
}
};
}, [
socket,
screenShot,
canvasRef,
isDOMMode,
screencastHandler,
rrwebSnapshotHandler,
domModeHandler,
screenshotModeHandler,
domModeErrorHandler,
]);
const domHighlighterHandler = useCallback(
(data: {
rect: DOMRect;
selector: string;
elementInfo: ElementInfo | null;
childSelectors?: string[];
isDOMMode?: boolean;
}) => {
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) {
const browserWindow = document.querySelector("#browser-window");
if (browserWindow) {
iframeElement = browserWindow.querySelector(
"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;
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,
};
if (getList === true) {
if (listSelector) {
socket?.emit("listSelector", { selector: listSelector });
const hasValidChildSelectors =
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 (
mappedData.childSelectors &&
mappedData.childSelectors.includes(mappedData.selector)
) {
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 {
setHighlighterData(null);
}
} else {
setHighlighterData(mappedData);
}
} else {
// getText mode
setHighlighterData(mappedData);
}
},
[
isDOMMode,
currentSnapshot,
getList,
socket,
listSelector,
paginationMode,
paginationType,
limitMode,
]
);
2024-06-14 21:47:42 +05:30
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
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(() => {
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[];
}) => {
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
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("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({});
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: cleanedSelector });
} else if (getList === true && listSelector && currentListId) {
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 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);
}
}
},
[
getText,
getList,
listSelector,
paginationMode,
paginationType,
fields,
currentListId,
currentTextActionId,
currentListActionId,
addTextStep,
addListStep,
notify,
socket,
t,
paginationSelector,
]
);
2024-08-09 08:54:26 +05:30
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;
2024-08-21 23:38:33 +05:30
const highlightRect = highlighterData.rect;
2025-03-14 12:35:35 +05:30
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(highlightRect);
if (
2025-03-14 12:35:35 +05:30
clickX >= mappedRect.left &&
clickX <= mappedRect.right &&
clickY >= mappedRect.top &&
clickY <= mappedRect.bottom
) {
const options = getAttributeOptions(highlighterData.elementInfo?.tagName || '', highlighterData.elementInfo);
2024-08-21 23:38:33 +05:30
2024-08-09 00:19:34 +05:30
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 || '' :
2024-08-21 23:38:33 +05:30
attribute === 'src' ? highlighterData.elementInfo?.imageUrl || '' :
highlighterData.elementInfo?.innerText || '';
addTextStep('', data, {
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
2025-05-20 17:44:00 +05:30
attribute,
2025-05-26 21:41:40 +05:30
}, currentTextActionId || `text-${crypto.randomUUID()}`);
} else {
// Show the modal if there are multiple options
2024-08-06 02:17:28 +05:30
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo,
2024-08-06 02:17:28 +05:30
});
setShowAttributeModal(true);
}
2024-08-09 09:25:43 +05:30
}
2024-08-21 23:38:33 +05:30
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'));
2025-05-26 21:41:40 +05:30
addListStep(listSelector!, fields, currentListId || 0, currentListActionId || `list-${crypto.randomUUID()}`, { type: paginationType, selector: highlighterData.selector });
socket?.emit('setPaginationMode', { pagination: false });
}
return;
}
if (getList === true && !listSelector) {
2025-01-31 00:42:19 +05:30
let cleanedSelector = highlighterData.selector;
if (cleanedSelector.includes('nth-child')) {
cleanedSelector = cleanedSelector.replace(/:nth-child\(\d+\)/g, '');
}
2025-01-31 01:08:07 +05:30
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;
2025-01-31 00:42:19 +05:30
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: {
2025-01-31 00:42:19 +05:30
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute
}
};
2025-05-07 09:18:08 +05:30
const updatedFields = {
...fields,
[newField.id]: newField
};
setFields(updatedFields);
2024-10-28 06:55:13 +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
}
} else {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo
});
setShowAttributeModal(true);
2024-08-09 06:23:10 +05:30
}
2024-08-04 03:18:41 +05:30
}
2024-10-27 19:25:10 +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 && (
<>
<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",
}}
/>
</>
)}
</>
)}
{isDOMMode ? (
currentSnapshot ? (
<DOMBrowserRenderer
width={dimensions.width}
height={dimensions.height}
snapshot={currentSnapshot}
getList={getList}
getText={getText}
listSelector={listSelector}
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',
};