feat: add child selector caching, revamp dom highlighting
This commit is contained in:
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { AuthContext } from '../../context/auth';
|
import { AuthContext } from '../../context/auth';
|
||||||
import { coordinateMapper } from '../../helpers/coordinateMapper';
|
import { coordinateMapper } from '../../helpers/coordinateMapper';
|
||||||
import { useBrowserDimensionsStore } from '../../context/browserDimensions';
|
import { useBrowserDimensionsStore } from '../../context/browserDimensions';
|
||||||
import { clientSelectorGenerator } from "../../helpers/clientSelectorGenerator";
|
import { clientSelectorGenerator, ElementFingerprint } from "../../helpers/clientSelectorGenerator";
|
||||||
import DatePicker from "../pickers/DatePicker";
|
import DatePicker from "../pickers/DatePicker";
|
||||||
import Dropdown from "../pickers/Dropdown";
|
import Dropdown from "../pickers/Dropdown";
|
||||||
import TimePicker from "../pickers/TimePicker";
|
import TimePicker from "../pickers/TimePicker";
|
||||||
@@ -147,7 +147,7 @@ export const BrowserWindow = () => {
|
|||||||
const { browserWidth, browserHeight } = useBrowserDimensionsStore();
|
const { browserWidth, browserHeight } = useBrowserDimensionsStore();
|
||||||
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
|
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
|
||||||
const [screenShot, setScreenShot] = useState<string>("");
|
const [screenShot, setScreenShot] = useState<string>("");
|
||||||
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
|
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 [showAttributeModal, setShowAttributeModal] = useState(false);
|
||||||
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
|
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
|
||||||
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
|
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
|
||||||
@@ -156,6 +156,7 @@ export const BrowserWindow = () => {
|
|||||||
const [isDOMMode, setIsDOMMode] = useState(false);
|
const [isDOMMode, setIsDOMMode] = useState(false);
|
||||||
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(null);
|
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [cachedChildSelectors, setCachedChildSelectors] = useState<string[]>([]);
|
||||||
|
|
||||||
const [listSelector, setListSelector] = useState<string | null>(null);
|
const [listSelector, setListSelector] = useState<string | null>(null);
|
||||||
const [fields, setFields] = useState<Record<string, TextStep>>({});
|
const [fields, setFields] = useState<Record<string, TextStep>>({});
|
||||||
@@ -168,6 +169,12 @@ export const BrowserWindow = () => {
|
|||||||
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
|
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
|
||||||
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
|
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
|
||||||
|
|
||||||
|
const [currentGroupInfo, setCurrentGroupInfo] = useState<{
|
||||||
|
isGroupElement: boolean;
|
||||||
|
groupSize: number;
|
||||||
|
groupElements: HTMLElement[];
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const { state } = useContext(AuthContext);
|
const { state } = useContext(AuthContext);
|
||||||
const { user } = state;
|
const { user } = state;
|
||||||
|
|
||||||
@@ -304,8 +311,23 @@ export const BrowserWindow = () => {
|
|||||||
socket?.emit("listSelector", { selector: listSelector });
|
socket?.emit("listSelector", { selector: listSelector });
|
||||||
|
|
||||||
clientSelectorGenerator.setListSelector(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]);
|
}
|
||||||
|
}
|
||||||
|
}, [isDOMMode, listSelector, socket, getList, currentSnapshot]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
|
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
|
||||||
@@ -345,6 +367,7 @@ export const BrowserWindow = () => {
|
|||||||
setListSelector(null);
|
setListSelector(null);
|
||||||
setFields({});
|
setFields({});
|
||||||
setCurrentListId(null);
|
setCurrentListId(null);
|
||||||
|
setCachedChildSelectors([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -408,8 +431,19 @@ export const BrowserWindow = () => {
|
|||||||
selector: string;
|
selector: string;
|
||||||
elementInfo: ElementInfo | null;
|
elementInfo: ElementInfo | null;
|
||||||
childSelectors?: string[];
|
childSelectors?: string[];
|
||||||
|
groupInfo?: {
|
||||||
|
isGroupElement: boolean;
|
||||||
|
groupSize: number;
|
||||||
|
groupElements: HTMLElement[];
|
||||||
|
groupFingerprint: ElementFingerprint;
|
||||||
|
};
|
||||||
isDOMMode?: boolean;
|
isDOMMode?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!getText && !getList) {
|
||||||
|
setHighlighterData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDOMMode || !currentSnapshot) {
|
if (!isDOMMode || !currentSnapshot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -424,15 +458,6 @@ export const BrowserWindow = () => {
|
|||||||
) as HTMLIFrameElement;
|
) as HTMLIFrameElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iframeElement) {
|
|
||||||
const browserWindow = document.querySelector("#browser-window");
|
|
||||||
if (browserWindow) {
|
|
||||||
iframeElement = browserWindow.querySelector(
|
|
||||||
"iframe"
|
|
||||||
) as HTMLIFrameElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iframeElement) {
|
if (!iframeElement) {
|
||||||
console.error("Could not find iframe element for DOM highlighting");
|
console.error("Could not find iframe element for DOM highlighting");
|
||||||
return;
|
return;
|
||||||
@@ -441,6 +466,12 @@ export const BrowserWindow = () => {
|
|||||||
const iframeRect = iframeElement.getBoundingClientRect();
|
const iframeRect = iframeElement.getBoundingClientRect();
|
||||||
const IFRAME_BODY_PADDING = 16;
|
const IFRAME_BODY_PADDING = 16;
|
||||||
|
|
||||||
|
if (data.groupInfo) {
|
||||||
|
setCurrentGroupInfo(data.groupInfo);
|
||||||
|
} else {
|
||||||
|
setCurrentGroupInfo(null);
|
||||||
|
}
|
||||||
|
|
||||||
const absoluteRect = new DOMRect(
|
const absoluteRect = new DOMRect(
|
||||||
data.rect.x + iframeRect.left - IFRAME_BODY_PADDING,
|
data.rect.x + iframeRect.left - IFRAME_BODY_PADDING,
|
||||||
data.rect.y + iframeRect.top - IFRAME_BODY_PADDING,
|
data.rect.y + iframeRect.top - IFRAME_BODY_PADDING,
|
||||||
@@ -451,12 +482,36 @@ export const BrowserWindow = () => {
|
|||||||
const mappedData = {
|
const mappedData = {
|
||||||
...data,
|
...data,
|
||||||
rect: absoluteRect,
|
rect: absoluteRect,
|
||||||
|
childSelectors: data.childSelectors || cachedChildSelectors,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (getList === true) {
|
if (getList === true) {
|
||||||
if (listSelector) {
|
if (!listSelector && data.groupInfo?.isGroupElement) {
|
||||||
socket?.emit("listSelector", { selector: listSelector });
|
const updatedGroupElements = data.groupInfo.groupElements.map(
|
||||||
const hasValidChildSelectors =
|
(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) &&
|
Array.isArray(mappedData.childSelectors) &&
|
||||||
mappedData.childSelectors.length > 0;
|
mappedData.childSelectors.length > 0;
|
||||||
|
|
||||||
@@ -471,62 +526,8 @@ export const BrowserWindow = () => {
|
|||||||
} else {
|
} else {
|
||||||
setHighlighterData(null);
|
setHighlighterData(null);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (hasChildSelectors) {
|
||||||
mappedData.childSelectors &&
|
|
||||||
mappedData.childSelectors.includes(mappedData.selector)
|
|
||||||
) {
|
|
||||||
setHighlighterData(mappedData);
|
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 {
|
} else {
|
||||||
setHighlighterData(null);
|
setHighlighterData(null);
|
||||||
}
|
}
|
||||||
@@ -534,23 +535,29 @@ export const BrowserWindow = () => {
|
|||||||
setHighlighterData(mappedData);
|
setHighlighterData(mappedData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// getText mode
|
|
||||||
setHighlighterData(mappedData);
|
setHighlighterData(mappedData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
isDOMMode,
|
isDOMMode,
|
||||||
currentSnapshot,
|
currentSnapshot,
|
||||||
|
getText,
|
||||||
getList,
|
getList,
|
||||||
socket,
|
socket,
|
||||||
listSelector,
|
listSelector,
|
||||||
paginationMode,
|
paginationMode,
|
||||||
paginationType,
|
paginationType,
|
||||||
limitMode,
|
limitMode,
|
||||||
|
cachedChildSelectors,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
|
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();
|
const now = performance.now();
|
||||||
if (now - highlighterUpdateRef.current < 16) {
|
if (now - highlighterUpdateRef.current < 16) {
|
||||||
return;
|
return;
|
||||||
@@ -652,6 +659,20 @@ export const BrowserWindow = () => {
|
|||||||
};
|
};
|
||||||
}, [socket, highlighterHandler, onMouseMove, getList, listSelector]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (socket && listSelector) {
|
if (socket && listSelector) {
|
||||||
console.log('Syncing list selector with server:', listSelector);
|
console.log('Syncing list selector with server:', listSelector);
|
||||||
@@ -673,11 +694,205 @@ export const BrowserWindow = () => {
|
|||||||
selector: string;
|
selector: string;
|
||||||
elementInfo: ElementInfo | null;
|
elementInfo: ElementInfo | null;
|
||||||
childSelectors?: string[];
|
childSelectors?: string[];
|
||||||
|
groupInfo?: {
|
||||||
|
isGroupElement: boolean;
|
||||||
|
groupSize: number;
|
||||||
|
groupElements: HTMLElement[];
|
||||||
|
};
|
||||||
}) => {
|
}) => {
|
||||||
setShowAttributeModal(false);
|
setShowAttributeModal(false);
|
||||||
setSelectedElement(null);
|
setSelectedElement(null);
|
||||||
setAttributeOptions([]);
|
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.group_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(
|
const options = getAttributeOptions(
|
||||||
highlighterData.elementInfo?.tagName || "",
|
highlighterData.elementInfo?.tagName || "",
|
||||||
highlighterData.elementInfo
|
highlighterData.elementInfo
|
||||||
@@ -742,47 +957,24 @@ export const BrowserWindow = () => {
|
|||||||
|
|
||||||
if (getList === true && !listSelector) {
|
if (getList === true && !listSelector) {
|
||||||
let cleanedSelector = highlighterData.selector;
|
let cleanedSelector = highlighterData.selector;
|
||||||
if (cleanedSelector.includes("nth-child")) {
|
if (
|
||||||
cleanedSelector = cleanedSelector.replace(/:nth-child\(\d+\)/g, "");
|
cleanedSelector.includes("[") &&
|
||||||
|
cleanedSelector.match(/\[\d+\]/)
|
||||||
|
) {
|
||||||
|
cleanedSelector = cleanedSelector.replace(/\[\d+\]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
setListSelector(cleanedSelector);
|
setListSelector(cleanedSelector);
|
||||||
notify(
|
notify(
|
||||||
`info`,
|
`info`,
|
||||||
t("browser_window.attribute_modal.notifications.list_select_success")
|
t(
|
||||||
|
"browser_window.attribute_modal.notifications.list_select_success"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
setCurrentListId(Date.now());
|
setCurrentListId(Date.now());
|
||||||
setFields({});
|
setFields({});
|
||||||
|
|
||||||
socket?.emit("setGetList", { getList: true });
|
|
||||||
socket?.emit("listSelector", { selector: cleanedSelector });
|
|
||||||
} else if (getList === true && listSelector && currentListId) {
|
} else if (getList === true && listSelector && currentListId) {
|
||||||
if (options.length === 1) {
|
|
||||||
const attribute = options[0].value;
|
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 =
|
const data =
|
||||||
attribute === "href"
|
attribute === "href"
|
||||||
? highlighterData.elementInfo?.url || ""
|
? highlighterData.elementInfo?.url || ""
|
||||||
@@ -790,6 +982,22 @@ export const BrowserWindow = () => {
|
|||||||
? highlighterData.elementInfo?.imageUrl || ""
|
? highlighterData.elementInfo?.imageUrl || ""
|
||||||
: highlighterData.elementInfo?.innerText || "";
|
: 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 = {
|
const newField: TextStep = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -828,150 +1036,6 @@ export const BrowserWindow = () => {
|
|||||||
setShowAttributeModal(true);
|
setShowAttributeModal(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[
|
|
||||||
getText,
|
|
||||||
getList,
|
|
||||||
listSelector,
|
|
||||||
paginationMode,
|
|
||||||
paginationType,
|
|
||||||
fields,
|
|
||||||
currentListId,
|
|
||||||
currentTextActionId,
|
|
||||||
currentListActionId,
|
|
||||||
addTextStep,
|
|
||||||
addListStep,
|
|
||||||
notify,
|
|
||||||
socket,
|
|
||||||
t,
|
|
||||||
paginationSelector,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const highlightRect = highlighterData.rect;
|
|
||||||
|
|
||||||
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(highlightRect);
|
|
||||||
if (
|
|
||||||
clickX >= mappedRect.left &&
|
|
||||||
clickX <= mappedRect.right &&
|
|
||||||
clickY >= mappedRect.top &&
|
|
||||||
clickY <= mappedRect.bottom
|
|
||||||
) {
|
|
||||||
|
|
||||||
const options = getAttributeOptions(highlighterData.elementInfo?.tagName || '', highlighterData.elementInfo);
|
|
||||||
|
|
||||||
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 || '' :
|
|
||||||
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 {
|
|
||||||
// Show the modal if there are multiple options
|
|
||||||
setAttributeOptions(options);
|
|
||||||
setSelectedElement({
|
|
||||||
selector: highlighterData.selector,
|
|
||||||
info: highlighterData.elementInfo,
|
|
||||||
});
|
|
||||||
setShowAttributeModal(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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'));
|
|
||||||
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({});
|
|
||||||
} 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;
|
|
||||||
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: {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1150,6 +1214,10 @@ export const BrowserWindow = () => {
|
|||||||
|
|
||||||
{isDOMMode && highlighterData && (
|
{isDOMMode && highlighterData && (
|
||||||
<>
|
<>
|
||||||
|
{/* Individual element highlight (for non-group or hovered element) */}
|
||||||
|
{(!getList ||
|
||||||
|
listSelector ||
|
||||||
|
!currentGroupInfo?.isGroupElement) && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -1172,6 +1240,59 @@ export const BrowserWindow = () => {
|
|||||||
transition: "all 0.1s ease-out",
|
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>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -1186,6 +1307,7 @@ export const BrowserWindow = () => {
|
|||||||
getList={getList}
|
getList={getList}
|
||||||
getText={getText}
|
getText={getText}
|
||||||
listSelector={listSelector}
|
listSelector={listSelector}
|
||||||
|
cachedChildSelectors={cachedChildSelectors}
|
||||||
paginationMode={paginationMode}
|
paginationMode={paginationMode}
|
||||||
paginationType={paginationType}
|
paginationType={paginationType}
|
||||||
limitMode={limitMode}
|
limitMode={limitMode}
|
||||||
|
|||||||
Reference in New Issue
Block a user