From 8940e8dbd63d8c20a62291659ad80619b0c94848 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 16 Jul 2025 00:27:47 +0530 Subject: [PATCH] feat: precompute selector mapping, shadow dom support --- src/components/browser/BrowserWindow.tsx | 774 ++++++++++++++--------- 1 file changed, 483 insertions(+), 291 deletions(-) diff --git a/src/components/browser/BrowserWindow.tsx b/src/components/browser/BrowserWindow.tsx index d3143f32..64a731a7 100644 --- a/src/components/browser/BrowserWindow.tsx +++ b/src/components/browser/BrowserWindow.tsx @@ -147,7 +147,18 @@ export const BrowserWindow = () => { 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 [highlighterData, setHighlighterData] = useState<{ + rect: DOMRect; + selector: string; + elementInfo: ElementInfo | null; + isShadow?: boolean; + childSelectors?: string[]; + groupElements?: Array<{ element: HTMLElement; rect: DOMRect }>; + similarElements?: { + elements: HTMLElement[]; + rects: DOMRect[]; + }; + } | null>(null); const [showAttributeModal, setShowAttributeModal] = useState(false); const [attributeOptions, setAttributeOptions] = useState([]); const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null); @@ -161,11 +172,20 @@ export const BrowserWindow = () => { const [paginationSelector, setPaginationSelector] = useState(''); const highlighterUpdateRef = useRef(0); + const [isCachingChildSelectors, setIsCachingChildSelectors] = useState(false); + const [cachedListSelector, setCachedListSelector] = useState( + null + ); + const [pendingNotification, setPendingNotification] = useState<{ + type: "error" | "warning" | "info" | "success"; + message: string; + count?: number; + } | null>(null); 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 { addTextStep, addListStep } = useBrowserSteps(); const [currentGroupInfo, setCurrentGroupInfo] = useState<{ isGroupElement: boolean; @@ -270,17 +290,6 @@ export const BrowserWindow = () => { [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) { @@ -300,28 +309,68 @@ export const BrowserWindow = () => { }, [isDOMMode, getList, listSelector, paginationMode]); useEffect(() => { - if (isDOMMode && listSelector) { - socket?.emit("setGetList", { getList: true }); - socket?.emit("listSelector", { selector: listSelector }); + if (isDOMMode && listSelector) { + socket?.emit("setGetList", { getList: true }); + socket?.emit("listSelector", { selector: listSelector }); - clientSelectorGenerator.setListSelector(listSelector); + clientSelectorGenerator.setListSelector(listSelector); - setCachedChildSelectors([]); + if (currentSnapshot && cachedListSelector !== listSelector) { + setCachedChildSelectors([]); + setIsCachingChildSelectors(true); + setCachedListSelector(listSelector); - if (currentSnapshot) { - const iframeElement = document.querySelector( - "#dom-browser-iframe" - ) as HTMLIFrameElement; - if (iframeElement?.contentDocument) { - const childSelectors = clientSelectorGenerator.getChildSelectors( - iframeElement.contentDocument, - listSelector - ); - setCachedChildSelectors(childSelectors); + const iframeElement = document.querySelector( + "#dom-browser-iframe" + ) as HTMLIFrameElement; + + if (iframeElement?.contentDocument) { + setTimeout(() => { + try { + const childSelectors = + clientSelectorGenerator.getChildSelectors( + iframeElement.contentDocument as Document, + listSelector + ); + + clientSelectorGenerator.precomputeChildSelectorMappings( + childSelectors, + iframeElement.contentDocument as Document + ); + + setCachedChildSelectors(childSelectors); + } catch (error) { + console.error("Error during child selector caching:", error); + } finally { + setIsCachingChildSelectors(false); + + if (pendingNotification) { + notify(pendingNotification.type, pendingNotification.message); + setPendingNotification(null); } - } + } + }, 100); + } else { + setIsCachingChildSelectors(false); + } } - }, [isDOMMode, listSelector, socket, getList, currentSnapshot]); + } + }, [ + isDOMMode, + listSelector, + socket, + getList, + currentSnapshot, + cachedListSelector, + pendingNotification, + notify, + ]); + + useEffect(() => { + if (!listSelector) { + setCachedListSelector(null); + } + }, [listSelector]); useEffect(() => { coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height); @@ -389,7 +438,6 @@ export const BrowserWindow = () => { 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); } @@ -403,7 +451,6 @@ export const BrowserWindow = () => { 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); } }; @@ -415,7 +462,6 @@ export const BrowserWindow = () => { screencastHandler, rrwebSnapshotHandler, domModeHandler, - // screenshotModeHandler, domModeErrorHandler, ]); @@ -425,12 +471,17 @@ export const BrowserWindow = () => { selector: string; elementInfo: ElementInfo | null; childSelectors?: string[]; + isShadow?: boolean; groupInfo?: { isGroupElement: boolean; groupSize: number; groupElements: HTMLElement[]; groupFingerprint: ElementFingerprint; }; + similarElements?: { + elements: HTMLElement[]; + rects: DOMRect[]; + }; isDOMMode?: boolean; }) => { if (!getText && !getList) { @@ -460,6 +511,22 @@ export const BrowserWindow = () => { const iframeRect = iframeElement.getBoundingClientRect(); const IFRAME_BODY_PADDING = 16; + let mappedSimilarElements; + if (data.similarElements) { + mappedSimilarElements = { + elements: data.similarElements.elements, + rects: data.similarElements.rects.map( + (rect) => + new DOMRect( + rect.x + iframeRect.left - IFRAME_BODY_PADDING, + rect.y + iframeRect.top - IFRAME_BODY_PADDING, + rect.width, + rect.height + ) + ), + }; + } + if (data.groupInfo) { setCurrentGroupInfo(data.groupInfo); } else { @@ -686,6 +753,7 @@ export const BrowserWindow = () => { (highlighterData: { rect: DOMRect; selector: string; + isShadow?: boolean; elementInfo: ElementInfo | null; childSelectors?: string[]; groupInfo?: { @@ -713,11 +781,17 @@ export const BrowserWindow = () => { ) ); addListStep( - listSelector!, - fields, - currentListId || 0, - currentListActionId || `list-${crypto.randomUUID()}`, - { type: paginationType, selector: highlighterData.selector } + listSelector!, + fields, + currentListId || 0, + currentListActionId || `list-${crypto.randomUUID()}`, + { + type: paginationType, + selector: highlighterData.selector, + isShadow: highlighterData.isShadow + }, + undefined, + highlighterData.isShadow ); socket?.emit("setPaginationMode", { pagination: false }); } @@ -776,7 +850,7 @@ export const BrowserWindow = () => { selectorObj: { selector: currentSelector, tag: highlighterData.elementInfo?.tagName, - shadow: highlighterData.elementInfo?.isShadowRoot, + isShadow: highlighterData.elementInfo?.isShadowRoot, attribute, }, }; @@ -794,7 +868,9 @@ export const BrowserWindow = () => { updatedFields, currentListId, currentListActionId || `list-${crypto.randomUUID()}`, - { type: "", selector: paginationSelector } + { type: "", selector: paginationSelector }, + undefined, + highlighterData.isShadow ); } } else { @@ -829,7 +905,7 @@ export const BrowserWindow = () => { { selector: highlighterData.selector, tag: highlighterData.elementInfo?.tagName, - shadow: highlighterData.elementInfo?.isShadowRoot, + isShadow: highlighterData.isShadow || highlighterData.elementInfo?.isShadowRoot, attribute, }, currentTextActionId || `text-${crypto.randomUUID()}` @@ -908,7 +984,7 @@ export const BrowserWindow = () => { { selector: highlighterData.selector, tag: highlighterData.elementInfo?.tagName, - shadow: highlighterData.elementInfo?.isShadowRoot, + isShadow: highlighterData.isShadow || highlighterData.elementInfo?.isShadowRoot, attribute, }, currentTextActionId || `text-${crypto.randomUUID()}` @@ -942,7 +1018,9 @@ export const BrowserWindow = () => { fields, currentListId || 0, currentListActionId || `list-${crypto.randomUUID()}`, - { type: paginationType, selector: highlighterData.selector } + { type: paginationType, selector: highlighterData.selector, isShadow: highlighterData.isShadow }, + undefined, + highlighterData.isShadow ); socket?.emit("setPaginationMode", { pagination: false }); } @@ -1000,7 +1078,7 @@ export const BrowserWindow = () => { selectorObj: { selector: currentSelector, tag: highlighterData.elementInfo?.tagName, - shadow: highlighterData.elementInfo?.isShadowRoot, + isShadow: highlighterData.elementInfo?.isShadowRoot, attribute, }, }; @@ -1018,7 +1096,9 @@ export const BrowserWindow = () => { updatedFields, currentListId, currentListActionId || `list-${crypto.randomUUID()}`, - { type: "", selector: paginationSelector } + { type: "", selector: paginationSelector, isShadow: highlighterData.isShadow }, + undefined, + highlighterData.isShadow ); } } else { @@ -1052,7 +1132,7 @@ export const BrowserWindow = () => { addTextStep('', data, { selector: selectedElement.selector, tag: selectedElement.info?.tagName, - shadow: selectedElement.info?.isShadowRoot, + isShadow: highlighterData?.isShadow || selectedElement.info?.isShadowRoot, attribute: attribute }, currentTextActionId || `text-${crypto.randomUUID()}`); } @@ -1065,7 +1145,7 @@ export const BrowserWindow = () => { selectorObj: { selector: selectedElement.selector, tag: selectedElement.info?.tagName, - shadow: selectedElement.info?.isShadowRoot, + isShadow: selectedElement.info?.isShadowRoot, attribute: attribute } }; @@ -1083,7 +1163,9 @@ export const BrowserWindow = () => { updatedFields, currentListId, currentListActionId || `list-${crypto.randomUUID()}`, - { type: '', selector: paginationSelector } + { type: "", selector: paginationSelector, isShadow: highlighterData?.isShadow }, + undefined, + highlighterData?.isShadow ); } } @@ -1110,260 +1192,370 @@ export const BrowserWindow = () => { }, [paginationMode, resetPaginationSelector]); return ( -
- { - getText === true || getList === true ? ( - { - setShowAttributeModal(false); - setSelectedElement(null); - setAttributeOptions([]); - }} - canBeClosed={true} - modalStyle={modalStyle} +
+ {/* Attribute selection modal */} + {(getText === true || getList === true) && ( + { + setShowAttributeModal(false); + setSelectedElement(null); + setAttributeOptions([]); + }} + canBeClosed={true} + modalStyle={modalStyle} + > +
+

Select Attribute

+
+ {attributeOptions.map((option) => ( + - ))} -
-
-
- ) : null - } + {option.label} + + + ))} +
+
+ + )} - {datePickerInfo && ( - setDatePickerInfo(null)} - /> - )} - {dropdownInfo && ( - setDropdownInfo(null)} - /> - )} - {timePickerInfo && ( - setTimePickerInfo(null)} - /> - )} - {dateTimeLocalInfo && ( - setDateTimeLocalInfo(null)} - /> - )} + {datePickerInfo && ( + setDatePickerInfo(null)} + /> + )} + {dropdownInfo && ( + setDropdownInfo(null)} + /> + )} + {timePickerInfo && ( + setTimePickerInfo(null)} + /> + )} + {dateTimeLocalInfo && ( + setDateTimeLocalInfo(null)} + /> + )} -
- {(getText === true || getList === true) && - !showAttributeModal && - highlighterData?.rect != null && ( - <> - {!isDOMMode && canvasRef?.current && ( - + {/* Main content area */} +
+ {/* Add CSS for the spinner animation */} + + + {(getText === true || getList === true) && + !showAttributeModal && + highlighterData?.rect != null && ( + <> + {!isDOMMode && canvasRef?.current && ( + + )} + + {isDOMMode && highlighterData && ( + <> + {/* Individual element highlight (for non-group or hovered element) */} + {getText && !listSelector && ( +
)} - {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 */} +
- {/* Group elements highlighting with real-time coordinates */} - {getList && - !listSelector && - currentGroupInfo?.isGroupElement && - highlighterData.groupElements && - highlighterData.groupElements.map((groupElement, index) => ( - - {/* Highlight box */} -
+
+ List item {index + 1} +
+ + ) + )} -
- List item {index + 1} -
- - ))} - - )} - + {getList && + listSelector && + !paginationMode && + !limitMode && + highlighterData?.similarElements && + highlighterData.similarElements.rects.map( + (rect, index) => ( + + {/* Highlight box for similar element */} +
+ + {/* Label for similar element */} +
+ Item {index + 1} +
+ + ) + )} + )} + + )} - {isDOMMode ? ( - currentSnapshot ? ( - { - domHighlighterHandler(data); - }} - onElementSelect={handleDOMElementSelection} - onShowDatePicker={handleShowDatePicker} - onShowDropdown={handleShowDropdown} - onShowTimePicker={handleShowTimePicker} - onShowDateTimePicker={handleShowDateTimePicker} - /> - ) : ( -
-
-
- Loading website... -
- -
- ) - ) : ( - /* Screenshot mode canvas */ - - )} + {isDOMMode ? ( +
+ {currentSnapshot ? ( + { + domHighlighterHandler(data); + }} + isCachingChildSelectors={isCachingChildSelectors} + onElementSelect={handleDOMElementSelection} + onShowDatePicker={handleShowDatePicker} + onShowDropdown={handleShowDropdown} + onShowTimePicker={handleShowTimePicker} + onShowDateTimePicker={handleShowDateTimePicker} + /> + ) : ( +
+
+
+ Loading website... +
+ +
+ )} + + {/* Loading overlay positioned specifically over DOM content */} + {isCachingChildSelectors && ( +
+
+
+ )}
+ ) : ( + /* Screenshot mode canvas */ + + )}
+
); };