import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useSocketStore } from '../../context/socket'; import { Button } from '@mui/material'; import Canvas from "../recorder/Canvas"; import { Highlighter } from "../recorder/Highlighter"; import { GenericModal } from '../ui/GenericModal'; import { useActionContext } from '../../context/browserActions'; import { useBrowserSteps, TextStep } from '../../context/browserSteps'; import { useGlobalInfoStore } from '../../context/globalInfo'; import { useTranslation } from 'react-i18next'; import { AuthContext } from '../../context/auth'; 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'; interface ElementInfo { tagName: string; hasOnlyText?: boolean; isIframeContent?: boolean; isShadowRoot?: boolean; innerText?: string; url?: string; imageUrl?: string; attributes?: Record; innerHTML?: string; outerHTML?: string; isDOMMode?: boolean; } interface AttributeOption { label: string; value: string; } interface ScreencastData { image: string; userId: string; viewport?: ViewportInfo | null; } interface ViewportInfo { width: number; height: number; } interface RRWebSnapshot { type: number; childNodes?: RRWebSnapshot[]; tagName?: string; attributes?: Record; 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; } const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): AttributeOption[] => { if (!elementInfo) return []; 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; 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; default: return [{ label: `Text: ${elementInfo.innerText}`, value: 'innerText' }]; } }; export const BrowserWindow = () => { const { t } = useTranslation(); const { browserWidth, browserHeight } = useBrowserDimensionsStore(); const [canvasRef, setCanvasReference] = useState | undefined>(undefined); const [screenShot, setScreenShot] = useState(""); const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], groupElements?: Array<{ element: HTMLElement; rect: DOMRect } >} | null>(null); const [showAttributeModal, setShowAttributeModal] = useState(false); const [attributeOptions, setAttributeOptions] = useState([]); const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null); const [currentListId, setCurrentListId] = useState(null); const [viewportInfo, setViewportInfo] = useState({ width: browserWidth, height: browserHeight }); const [isLoading, setIsLoading] = useState(false); const [cachedChildSelectors, setCachedChildSelectors] = useState([]); const [listSelector, setListSelector] = useState(null); const [fields, setFields] = useState>({}); const [paginationSelector, setPaginationSelector] = useState(''); const highlighterUpdateRef = useRef(0); const { socket } = useSocketStore(); const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode, currentSnapshot } = useGlobalInfoStore(); const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext(); const { addTextStep, addListStep, updateListStepData } = useBrowserSteps(); const [currentGroupInfo, setCurrentGroupInfo] = useState<{ isGroupElement: boolean; groupSize: number; groupElements: HTMLElement[]; } | null>(null); const { state } = useContext(AuthContext); const { user } = state; 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 }; 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) { updateDOMMode(true, data.snapshotData); socket?.emit("dom-mode-enabled"); setIsLoading(false); } else { setIsLoading(false); } } }, [user?.id, socket, updateDOMMode] ); const domModeHandler = useCallback( (data: any) => { if (!data.userId || data.userId === user?.id) { updateDOMMode(true); socket?.emit("dom-mode-enabled"); setIsLoading(false); } }, [user?.id, socket, updateDOMMode] ); const screenshotModeHandler = useCallback( (data: any) => { if (!data.userId || data.userId === user?.id) { updateDOMMode(false); socket?.emit("screenshot-mode-enabled"); setIsLoading(false); } }, [user?.id, updateDOMMode] ); const domModeErrorHandler = useCallback( (data: any) => { if (!data.userId || data.userId === user?.id) { updateDOMMode(false); setIsLoading(false); } }, [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]); useEffect(() => { coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height); }, [viewportInfo, dimensions.width, dimensions.height]); useEffect(() => { if (listSelector) { sessionStorage.setItem('recordingListSelector', listSelector); } }, [listSelector]); useEffect(() => { const storedListSelector = sessionStorage.getItem('recordingListSelector'); // Only restore state if it exists in sessionStorage if (storedListSelector && !listSelector) { setListSelector(storedListSelector); } }, []); const onMouseMove = (e: MouseEvent) => { if (canvasRef && canvasRef.current && highlighterData) { const canvasRect = canvasRef.current.getBoundingClientRect(); // mousemove outside the browser window 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([]); }, []); useEffect(() => { if (!getList) { resetListState(); } }, [getList, resetListState]); 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); if (data.viewport) { setViewportInfo(data.viewport); } } } }, [screenShot, user?.id]); useEffect(() => { 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); } if (canvasRef?.current && !isDOMMode && screenShot) { drawImage(screenShot, canvasRef.current); } 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[]; 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, ] ); 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; // 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 }; 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) { // Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp' if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) { setHighlighterData(mappedData); } else { setHighlighterData(null); } } else if (mappedData.childSelectors && mappedData.childSelectors.includes(mappedData.selector)) { // Highlight only valid child elements within the listSelector setHighlighterData(mappedData); } else if (mappedData.elementInfo?.isIframeContent && mappedData.childSelectors) { // Handle iframe elements 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) { // Handle mixed DOM cases with iframes 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) { // Handle Shadow DOM elements 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) { // Handle mixed DOM cases 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 not a valid child in normal mode, clear the highlighter setHighlighterData(null); } } else { // Set highlighterData for the initial listSelector selection setHighlighterData(mappedData); } } else { // For non-list steps setHighlighterData(mappedData); } }, [getList, socket, listSelector, paginationMode, paginationType, limitMode]); 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, onMouseMove, getList, listSelector]); useEffect(() => { document.addEventListener("mousemove", onMouseMove, false); if (socket) { socket.off("highlighter", highlighterHandler); socket.on("highlighter", highlighterHandler); } return () => { document.removeEventListener("mousemove", onMouseMove); if (socket) { socket.off("highlighter", highlighterHandler); } }; }, [socket, highlighterHandler, getList, listSelector]); useEffect(() => { if (socket && listSelector) { console.log('Syncing list selector with server:', listSelector); socket.emit('setGetList', { getList: true }); socket.emit('listSelector', { selector: listSelector }); } }, [socket, listSelector]); 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( "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) => { 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); } } } } }; 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 || ''; } { if (getText === true) { addTextStep('', data, { selector: selectedElement.selector, tag: selectedElement.info?.tagName, shadow: selectedElement.info?.isShadowRoot, attribute: attribute }, currentTextActionId || `text-${crypto.randomUUID()}`); } 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 } }; const updatedFields = { ...fields, [newField.id]: newField }; setFields(updatedFields); if (listSelector) { addListStep( listSelector, updatedFields, currentListId, currentListActionId || `list-${crypto.randomUUID()}`, { type: '', selector: paginationSelector } ); } } } } setShowAttributeModal(false); setSelectedElement(null); setAttributeOptions([]); setTimeout(() => { setShowAttributeModal(false); }, 0); }; const resetPaginationSelector = useCallback(() => { setPaginationSelector(''); }, []); useEffect(() => { if (!paginationMode) { resetPaginationSelector(); } }, [paginationMode, resetPaginationSelector]); return (
{ getText === true || getList === true ? ( { setShowAttributeModal(false); setSelectedElement(null); setAttributeOptions([]); }} canBeClosed={true} modalStyle={modalStyle} >

Select Attribute

{attributeOptions.map((option) => ( ))}
) : null } {datePickerInfo && ( setDatePickerInfo(null)} /> )} {dropdownInfo && ( setDropdownInfo(null)} /> )} {timePickerInfo && ( setTimePickerInfo(null)} /> )} {dateTimeLocalInfo && ( setDateTimeLocalInfo(null)} /> )}
{(getText === true || getList === true) && !showAttributeModal && highlighterData?.rect != null && ( <> {!isDOMMode && canvasRef?.current && ( )} {isDOMMode && highlighterData && ( <> {/* Individual element highlight (for non-group or hovered element) */} {(!getList || listSelector || !currentGroupInfo?.isGroupElement) && (
)} {/* Group elements highlighting with real-time coordinates */} {getList && !listSelector && currentGroupInfo?.isGroupElement && highlighterData.groupElements && highlighterData.groupElements.map((groupElement, index) => ( {/* Highlight box */}
List item {index + 1}
))} )} )} {isDOMMode ? ( currentSnapshot ? ( { domHighlighterHandler(data); }} onElementSelect={handleDOMElementSelection} onShowDatePicker={handleShowDatePicker} onShowDropdown={handleShowDropdown} onShowTimePicker={handleShowTimePicker} onShowDateTimePicker={handleShowDateTimePicker} /> ) : (
Loading website...
) ) : ( /* Screenshot mode canvas */ )}
); }; const drawImage = (image: string, canvas: HTMLCanvasElement): void => { const ctx = canvas.getContext('2d'); if (!ctx) return; const img = new Image(); img.onload = () => { requestAnimationFrame(() => { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }); if (image.startsWith('blob:')) { URL.revokeObjectURL(image); } }; img.onerror = () => { console.warn('Failed to load image'); }; img.src = image; }; const modalStyle = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '30%', backgroundColor: 'background.paper', p: 4, height: 'fit-content', display: 'block', padding: '20px', };