Merge branch 'develop' into iframe
This commit is contained in:
@@ -205,50 +205,66 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findAllElements(config) {
|
function findAllElements(config) {
|
||||||
// Check if selector contains iframe notation (:>>)
|
// Regular DOM query if no special delimiters
|
||||||
if (!config.selector.includes(':>>')) {
|
if (!config.selector.includes('>>') && !config.selector.includes(':>>')) {
|
||||||
return Array.from(document.querySelectorAll(config.selector));
|
return Array.from(document.querySelectorAll(config.selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
// For iframe traversal, split by iframe boundary marker
|
// Split by both types of delimiters
|
||||||
const parts = config.selector.split(':>>').map(s => s.trim());
|
const parts = config.selector.split(/(?:>>|:>>)/).map(s => s.trim());
|
||||||
|
const delimiters = config.selector.match(/(?:>>|:>>)/g) || [];
|
||||||
let currentElements = [document];
|
let currentElements = [document];
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
const nextElements = [];
|
const nextElements = [];
|
||||||
|
const isLast = i === parts.length - 1;
|
||||||
|
const delimiter = delimiters[i] || '';
|
||||||
|
const isIframeTraversal = delimiter === ':>>';
|
||||||
|
|
||||||
for (const element of currentElements) {
|
for (const element of currentElements) {
|
||||||
try {
|
try {
|
||||||
let targets;
|
let targets;
|
||||||
|
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
// First selector is queried from main document
|
// First selector is queried from main document
|
||||||
targets = Array.from(element.querySelectorAll(part))
|
targets = Array.from(element.querySelectorAll(part))
|
||||||
.filter(el => {
|
.filter(el => {
|
||||||
// Only include iframes if not the last part
|
if (isLast) return true;
|
||||||
if (i === parts.length - 1) return true;
|
// For iframe traversal, only include iframes
|
||||||
return el.tagName === 'IFRAME';
|
if (isIframeTraversal) return el.tagName === 'IFRAME';
|
||||||
});
|
// For shadow DOM traversal, only include elements with shadow root
|
||||||
|
return el.shadowRoot && el.shadowRoot.mode === 'open';
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// For subsequent selectors, we need to look inside iframes
|
if (isIframeTraversal) {
|
||||||
|
// Handle iframe traversal
|
||||||
const iframeDocument = element.contentDocument || element.contentWindow?.document;
|
const iframeDocument = element.contentDocument || element.contentWindow?.document;
|
||||||
if (!iframeDocument) continue;
|
if (!iframeDocument) continue;
|
||||||
|
|
||||||
targets = Array.from(iframeDocument.querySelectorAll(part));
|
targets = Array.from(iframeDocument.querySelectorAll(part));
|
||||||
|
if (!isLast) {
|
||||||
// If this isn't the last part, filter for iframes only
|
targets = targets.filter(el => el.tagName === 'IFRAME');
|
||||||
if (i < parts.length - 1) {
|
|
||||||
targets = targets.filter(el => el.tagName === 'IFRAME');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Handle shadow DOM traversal
|
||||||
|
const shadowRoot = element.shadowRoot;
|
||||||
|
if (!shadowRoot || shadowRoot.mode !== 'open') continue;
|
||||||
|
|
||||||
|
targets = Array.from(shadowRoot.querySelectorAll(part));
|
||||||
|
if (!isLast) {
|
||||||
|
targets = targets.filter(el => el.shadowRoot && el.shadowRoot.mode === 'open');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextElements.push(...targets);
|
nextElements.push(...targets);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle cross-origin iframe access errors
|
console.warn('Cannot access content:', error);
|
||||||
console.warn('Cannot access iframe content:', error);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextElements.length === 0) return [];
|
if (nextElements.length === 0) return [];
|
||||||
currentElements = nextElements;
|
currentElements = nextElements;
|
||||||
}
|
}
|
||||||
@@ -291,26 +307,27 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
|||||||
)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find minimal bounding elements
|
||||||
function getMBEs(elements) {
|
function getMBEs(elements) {
|
||||||
return elements.map((element) => {
|
return elements.map((element) => {
|
||||||
let candidate = element;
|
let candidate = element;
|
||||||
const isUniqueChild = (e) => elements
|
const isUniqueChild = (e) => elements
|
||||||
.filter((elem) => {
|
.filter((elem) => {
|
||||||
// Handle iframe boundaries when checking containment
|
// Handle both iframe and shadow DOM boundaries
|
||||||
const sameDocument = elem.ownerDocument === e.ownerDocument;
|
const sameContext = elem.getRootNode() === e.getRootNode() &&
|
||||||
return sameDocument && e.parentNode?.contains(elem);
|
elem.ownerDocument === e.ownerDocument;
|
||||||
})
|
return sameContext && e.parentNode?.contains(elem);
|
||||||
.length === 1;
|
})
|
||||||
|
.length === 1;
|
||||||
while (candidate && isUniqueChild(candidate)) {
|
|
||||||
candidate = candidate.parentNode;
|
while (candidate && isUniqueChild(candidate)) {
|
||||||
}
|
candidate = candidate.parentNode;
|
||||||
|
}
|
||||||
return candidate;
|
|
||||||
|
return candidate;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main scraping logic remains the same
|
|
||||||
const seedName = getSeedKey(lists);
|
const seedName = getSeedKey(lists);
|
||||||
const seedElements = findAllElements(lists[seedName]);
|
const seedElements = findAllElements(lists[seedName]);
|
||||||
const MBEs = getMBEs(seedElements);
|
const MBEs = getMBEs(seedElements);
|
||||||
@@ -364,144 +381,207 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
|||||||
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
|
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
|
||||||
*/
|
*/
|
||||||
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
|
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
|
||||||
// Helper function to query elements within an iframe
|
// Enhanced query function to handle both iframe and shadow DOM
|
||||||
const queryIframe = (rootElement, selector) => {
|
const queryElement = (rootElement, selector) => {
|
||||||
if (!selector.includes(':>>')) {
|
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||||
return rootElement.querySelector(selector);
|
return rootElement.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = selector.split(':>>').map(part => part.trim());
|
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||||
let currentElement = rootElement;
|
let currentElement = rootElement;
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (!currentElement) return null;
|
if (!currentElement) return null;
|
||||||
|
|
||||||
// Handle iframe content document
|
// Handle iframe traversal
|
||||||
if (currentElement.tagName === 'IFRAME') {
|
if (currentElement.tagName === 'IFRAME') {
|
||||||
try {
|
try {
|
||||||
const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
|
const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
|
||||||
currentElement = iframeDoc.querySelector(parts[i]);
|
currentElement = iframeDoc.querySelector(parts[i]);
|
||||||
continue;
|
continue;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Cannot access iframe content:', e);
|
console.warn('Cannot access iframe content:', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentElement = currentElement.querySelector(parts[i]);
|
// Try regular DOM first
|
||||||
}
|
let nextElement = currentElement.querySelector(parts[i]);
|
||||||
|
|
||||||
return currentElement;
|
// Try shadow DOM if not found
|
||||||
|
if (!nextElement && currentElement.shadowRoot) {
|
||||||
|
nextElement = currentElement.shadowRoot.querySelector(parts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children's shadow roots if still not found
|
||||||
|
if (!nextElement) {
|
||||||
|
const children = Array.from(currentElement.children || []);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.shadowRoot) {
|
||||||
|
nextElement = child.shadowRoot.querySelector(parts[i]);
|
||||||
|
if (nextElement) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElement = nextElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to query all matching elements within iframes
|
// Enhanced query all function for both contexts
|
||||||
const queryIframeAll = (rootElement, selector) => {
|
const queryElementAll = (rootElement, selector) => {
|
||||||
if (!selector.includes(':>>')) {
|
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||||
return rootElement.querySelectorAll(selector);
|
return rootElement.querySelectorAll(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = selector.split(':>>').map(part => part.trim());
|
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||||
let currentElements = [rootElement];
|
let currentElements = [rootElement];
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
const nextElements = [];
|
const nextElements = [];
|
||||||
|
|
||||||
for (const element of currentElements) {
|
for (const element of currentElements) {
|
||||||
if (element.tagName === 'IFRAME') {
|
// Handle iframe traversal
|
||||||
try {
|
if (element.tagName === 'IFRAME') {
|
||||||
const iframeDoc = element.contentDocument || element.contentWindow.document;
|
try {
|
||||||
nextElements.push(...iframeDoc.querySelectorAll(part));
|
const iframeDoc = element.contentDocument || element.contentWindow.document;
|
||||||
} catch (e) {
|
nextElements.push(...iframeDoc.querySelectorAll(part));
|
||||||
console.error('Cannot access iframe content:', e);
|
} catch (e) {
|
||||||
continue;
|
console.warn('Cannot access iframe content:', e);
|
||||||
}
|
continue;
|
||||||
} else {
|
}
|
||||||
nextElements.push(...element.querySelectorAll(part));
|
} else {
|
||||||
}
|
// Regular DOM elements
|
||||||
}
|
if (element.querySelectorAll) {
|
||||||
|
nextElements.push(...element.querySelectorAll(part));
|
||||||
currentElements = nextElements;
|
}
|
||||||
}
|
|
||||||
|
// Shadow DOM elements
|
||||||
return currentElements;
|
if (element.shadowRoot) {
|
||||||
|
nextElements.push(...element.shadowRoot.querySelectorAll(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children's shadow roots
|
||||||
|
const children = Array.from(element.children || []);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.shadowRoot) {
|
||||||
|
nextElements.push(...child.shadowRoot.querySelectorAll(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElements = nextElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to extract values from elements
|
// Enhanced value extraction with context awareness
|
||||||
function extractValue(element, attribute) {
|
function extractValue(element, attribute) {
|
||||||
if (!element) return null;
|
if (!element) return null;
|
||||||
|
|
||||||
if (attribute === 'innerText') {
|
// Get context-aware base URL
|
||||||
return element.innerText.trim();
|
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||||
} else if (attribute === 'innerHTML') {
|
|
||||||
return element.innerHTML.trim();
|
// Check shadow root first
|
||||||
} else if (attribute === 'src' || attribute === 'href') {
|
if (element.shadowRoot) {
|
||||||
const attrValue = element.getAttribute(attribute);
|
const shadowContent = element.shadowRoot.textContent;
|
||||||
return attrValue ? new URL(attrValue, window.location.origin).href : null;
|
if (shadowContent?.trim()) {
|
||||||
}
|
return shadowContent.trim();
|
||||||
return element.getAttribute(attribute);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute === 'innerText') {
|
||||||
|
return element.innerText.trim();
|
||||||
|
} else if (attribute === 'innerHTML') {
|
||||||
|
return element.innerHTML.trim();
|
||||||
|
} else if (attribute === 'src' || attribute === 'href') {
|
||||||
|
const attrValue = element.getAttribute(attribute);
|
||||||
|
return attrValue ? new URL(attrValue, baseURL).href : null;
|
||||||
|
}
|
||||||
|
return element.getAttribute(attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to find table ancestor elements
|
// Enhanced table ancestor finding with context support
|
||||||
function findTableAncestor(element) {
|
function findTableAncestor(element) {
|
||||||
let currentElement = element;
|
let currentElement = element;
|
||||||
const MAX_DEPTH = 5;
|
const MAX_DEPTH = 5;
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
|
||||||
while (currentElement && depth < MAX_DEPTH) {
|
while (currentElement && depth < MAX_DEPTH) {
|
||||||
if (currentElement.tagName === 'TD') {
|
// Handle shadow DOM
|
||||||
return { type: 'TD', element: currentElement };
|
if (currentElement.getRootNode() instanceof ShadowRoot) {
|
||||||
} else if (currentElement.tagName === 'TR') {
|
currentElement = currentElement.getRootNode().host;
|
||||||
return { type: 'TR', element: currentElement };
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle iframe boundary crossing
|
if (currentElement.tagName === 'TD') {
|
||||||
if (currentElement.tagName === 'IFRAME') {
|
return { type: 'TD', element: currentElement };
|
||||||
try {
|
} else if (currentElement.tagName === 'TR') {
|
||||||
currentElement = currentElement.contentDocument.body;
|
return { type: 'TR', element: currentElement };
|
||||||
} catch (e) {
|
}
|
||||||
return null;
|
|
||||||
}
|
// Handle iframe crossing
|
||||||
} else {
|
if (currentElement.tagName === 'IFRAME') {
|
||||||
currentElement = currentElement.parentElement;
|
try {
|
||||||
}
|
currentElement = currentElement.contentDocument.body;
|
||||||
depth++;
|
} catch (e) {
|
||||||
}
|
return null;
|
||||||
return null;
|
}
|
||||||
|
} else {
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get cell index
|
// Helper function to get cell index
|
||||||
function getCellIndex(td) {
|
function getCellIndex(td) {
|
||||||
let index = 0;
|
if (td.getRootNode() instanceof ShadowRoot) {
|
||||||
let sibling = td;
|
const shadowRoot = td.getRootNode();
|
||||||
while (sibling = sibling.previousElementSibling) {
|
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
|
||||||
index++;
|
return allCells.indexOf(td);
|
||||||
}
|
}
|
||||||
return index;
|
|
||||||
|
let index = 0;
|
||||||
|
let sibling = td;
|
||||||
|
while (sibling = sibling.previousElementSibling) {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check for TH elements
|
// Helper function to check for TH elements
|
||||||
function hasThElement(row, tableFields) {
|
function hasThElement(row, tableFields) {
|
||||||
for (const [label, { selector }] of Object.entries(tableFields)) {
|
for (const [_, { selector }] of Object.entries(tableFields)) {
|
||||||
const element = queryIframe(row, selector);
|
const element = queryElement(row, selector);
|
||||||
if (element) {
|
if (element) {
|
||||||
let current = element;
|
let current = element;
|
||||||
while (current && current !== row) {
|
while (current && current !== row) {
|
||||||
if (current.tagName === 'TH') {
|
if (current.getRootNode() instanceof ShadowRoot) {
|
||||||
return true;
|
current = current.getRootNode().host;
|
||||||
}
|
continue;
|
||||||
if (current.tagName === 'IFRAME') {
|
}
|
||||||
try {
|
|
||||||
current = current.contentDocument.body;
|
if (current.tagName === 'TH') return true;
|
||||||
} catch (e) {
|
|
||||||
break;
|
if (current.tagName === 'IFRAME') {
|
||||||
}
|
try {
|
||||||
} else {
|
current = current.contentDocument.body;
|
||||||
current = current.parentElement;
|
} catch (e) {
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
current = current.parentElement;
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to filter rows
|
// Helper function to filter rows
|
||||||
@@ -511,7 +591,13 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows.filter(row => row.getElementsByTagName('TH').length === 0);
|
// Include shadow DOM in TH search
|
||||||
|
return rows.filter(row => {
|
||||||
|
const directTH = row.getElementsByTagName('TH').length === 0;
|
||||||
|
const shadowTH = row.shadowRoot ?
|
||||||
|
row.shadowRoot.querySelector('th') === null : true;
|
||||||
|
return directTH && shadowTH;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Class similarity comparison functions
|
// Class similarity comparison functions
|
||||||
@@ -523,188 +609,231 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
|||||||
return intersection.size / union.size;
|
return intersection.size / union.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced similar elements finding with context support
|
||||||
function findSimilarElements(baseElement, similarityThreshold = 0.7) {
|
function findSimilarElements(baseElement, similarityThreshold = 0.7) {
|
||||||
const baseClasses = Array.from(baseElement.classList);
|
const baseClasses = Array.from(baseElement.classList);
|
||||||
if (baseClasses.length === 0) return [];
|
if (baseClasses.length === 0) return [];
|
||||||
|
|
||||||
// Include elements from all iframes
|
const allElements = [];
|
||||||
const allElements = [];
|
|
||||||
const iframes = document.getElementsByTagName('iframe');
|
// Get elements from main document
|
||||||
|
allElements.push(...document.getElementsByTagName(baseElement.tagName));
|
||||||
// Add elements from main document
|
|
||||||
allElements.push(...document.getElementsByTagName(baseElement.tagName));
|
// Get elements from shadow DOM
|
||||||
|
if (baseElement.getRootNode() instanceof ShadowRoot) {
|
||||||
// Add elements from each iframe
|
const shadowHost = baseElement.getRootNode().host;
|
||||||
for (const iframe of iframes) {
|
allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName));
|
||||||
try {
|
}
|
||||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
|
||||||
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
|
// Get elements from iframes
|
||||||
} catch (e) {
|
const iframes = document.getElementsByTagName('iframe');
|
||||||
console.error('Cannot access iframe content:', e);
|
for (const iframe of iframes) {
|
||||||
}
|
try {
|
||||||
}
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
|
||||||
return allElements.filter(element => {
|
} catch (e) {
|
||||||
if (element === baseElement) return false;
|
console.warn('Cannot access iframe content:', e);
|
||||||
const similarity = calculateClassSimilarity(
|
}
|
||||||
baseClasses,
|
}
|
||||||
Array.from(element.classList)
|
|
||||||
);
|
return allElements.filter(element => {
|
||||||
return similarity >= similarityThreshold;
|
if (element === baseElement) return false;
|
||||||
});
|
const similarity = calculateClassSimilarity(
|
||||||
|
baseClasses,
|
||||||
|
Array.from(element.classList)
|
||||||
|
);
|
||||||
|
return similarity >= similarityThreshold;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main scraping logic
|
// Main scraping logic with context support
|
||||||
let containers = queryIframeAll(document, listSelector);
|
let containers = queryElementAll(document, listSelector);
|
||||||
containers = Array.from(containers);
|
containers = Array.from(containers);
|
||||||
|
|
||||||
if (containers.length === 0) return [];
|
if (containers.length === 0) return [];
|
||||||
|
|
||||||
if (limit > 1 && containers.length === 1) {
|
if (limit > 1 && containers.length === 1) {
|
||||||
const baseContainer = containers[0];
|
const baseContainer = containers[0];
|
||||||
const similarContainers = findSimilarElements(baseContainer);
|
const similarContainers = findSimilarElements(baseContainer);
|
||||||
|
|
||||||
if (similarContainers.length > 0) {
|
if (similarContainers.length > 0) {
|
||||||
const newContainers = similarContainers.filter(container =>
|
const newContainers = similarContainers.filter(container =>
|
||||||
!container.matches(listSelector)
|
!container.matches(listSelector)
|
||||||
);
|
);
|
||||||
containers = [...containers, ...newContainers];
|
containers = [...containers, ...newContainers];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerFields = containers.map(() => ({
|
const containerFields = containers.map(() => ({
|
||||||
tableFields: {},
|
tableFields: {},
|
||||||
nonTableFields: {}
|
nonTableFields: {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Classify fields
|
// Classify fields
|
||||||
containers.forEach((container, containerIndex) => {
|
containers.forEach((container, containerIndex) => {
|
||||||
for (const [label, field] of Object.entries(fields)) {
|
for (const [label, field] of Object.entries(fields)) {
|
||||||
const sampleElement = queryIframe(container, field.selector);
|
const sampleElement = queryElement(container, field.selector);
|
||||||
|
|
||||||
if (sampleElement) {
|
if (sampleElement) {
|
||||||
const ancestor = findTableAncestor(sampleElement);
|
const ancestor = findTableAncestor(sampleElement);
|
||||||
if (ancestor) {
|
if (ancestor) {
|
||||||
containerFields[containerIndex].tableFields[label] = {
|
containerFields[containerIndex].tableFields[label] = {
|
||||||
...field,
|
...field,
|
||||||
tableContext: ancestor.type,
|
tableContext: ancestor.type,
|
||||||
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
containerFields[containerIndex].nonTableFields[label] = field;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
containerFields[containerIndex].nonTableFields[label] = field;
|
containerFields[containerIndex].nonTableFields[label] = field;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
containerFields[containerIndex].nonTableFields[label] = field;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableData = [];
|
const tableData = [];
|
||||||
const nonTableData = [];
|
const nonTableData = [];
|
||||||
|
|
||||||
// Process table data
|
// Process table data with both iframe and shadow DOM support
|
||||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||||
const container = containers[containerIndex];
|
const container = containers[containerIndex];
|
||||||
const { tableFields } = containerFields[containerIndex];
|
const { tableFields } = containerFields[containerIndex];
|
||||||
|
|
||||||
if (Object.keys(tableFields).length > 0) {
|
if (Object.keys(tableFields).length > 0) {
|
||||||
const firstField = Object.values(tableFields)[0];
|
const firstField = Object.values(tableFields)[0];
|
||||||
const firstElement = queryIframe(container, firstField.selector);
|
const firstElement = queryElement(container, firstField.selector);
|
||||||
let tableContext = firstElement;
|
let tableContext = firstElement;
|
||||||
|
|
||||||
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
// Find table context including both iframe and shadow DOM
|
||||||
if (tableContext.tagName === 'IFRAME') {
|
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||||
try {
|
if (tableContext.getRootNode() instanceof ShadowRoot) {
|
||||||
tableContext = tableContext.contentDocument.body;
|
tableContext = tableContext.getRootNode().host;
|
||||||
} catch (e) {
|
continue;
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
} else {
|
if (tableContext.tagName === 'IFRAME') {
|
||||||
tableContext = tableContext.parentElement;
|
try {
|
||||||
}
|
tableContext = tableContext.contentDocument.body;
|
||||||
}
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tableContext = tableContext.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tableContext) {
|
if (tableContext) {
|
||||||
const rows = Array.from(tableContext.getElementsByTagName('TR'));
|
// Get rows from all contexts
|
||||||
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
const rows = [];
|
||||||
|
|
||||||
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
// Get rows from regular DOM
|
||||||
const record = {};
|
rows.push(...tableContext.getElementsByTagName('TR'));
|
||||||
const currentRow = processedRows[rowIndex];
|
|
||||||
|
// Get rows from shadow DOM
|
||||||
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
if (tableContext.shadowRoot) {
|
||||||
let element = null;
|
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
|
||||||
|
}
|
||||||
if (cellIndex >= 0) {
|
|
||||||
const td = currentRow.children[cellIndex];
|
// Get rows from iframes
|
||||||
if (td) {
|
if (tableContext.tagName === 'IFRAME') {
|
||||||
element = queryIframe(td, selector);
|
try {
|
||||||
|
const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document;
|
||||||
if (!element && selector.split(">").pop().includes('td:nth-child')) {
|
rows.push(...iframeDoc.getElementsByTagName('TR'));
|
||||||
element = td;
|
} catch (e) {
|
||||||
}
|
console.warn('Cannot access iframe rows:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||||
|
const record = {};
|
||||||
|
const currentRow = processedRows[rowIndex];
|
||||||
|
|
||||||
|
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
if (cellIndex >= 0) {
|
||||||
|
// Get TD element considering both contexts
|
||||||
|
let td = currentRow.children[cellIndex];
|
||||||
|
|
||||||
|
// Check shadow DOM for td
|
||||||
|
if (!td && currentRow.shadowRoot) {
|
||||||
|
const shadowCells = currentRow.shadowRoot.children;
|
||||||
|
if (shadowCells && shadowCells.length > cellIndex) {
|
||||||
|
td = shadowCells[cellIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td) {
|
||||||
|
element = queryElement(td, selector);
|
||||||
|
|
||||||
|
if (!element && selector.split(/(?:>>|:>>)/).pop().includes('td:nth-child')) {
|
||||||
|
element = td;
|
||||||
|
}
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
const tagOnlySelector = selector.split('.')[0];
|
const tagOnlySelector = selector.split('.')[0];
|
||||||
element = queryIframe(td, tagOnlySelector);
|
element = queryElement(td, tagOnlySelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
let currentElement = td;
|
let currentElement = td;
|
||||||
while (currentElement && currentElement.children.length > 0) {
|
while (currentElement && currentElement.children.length > 0) {
|
||||||
let foundContentChild = false;
|
let foundContentChild = false;
|
||||||
for (const child of currentElement.children) {
|
for (const child of currentElement.children) {
|
||||||
if (extractValue(child, attribute)) {
|
if (extractValue(child, attribute)) {
|
||||||
currentElement = child;
|
currentElement = child;
|
||||||
foundContentChild = true;
|
foundContentChild = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundContentChild) break;
|
if (!foundContentChild) break;
|
||||||
}
|
}
|
||||||
element = currentElement;
|
element = currentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
element = queryIframe(currentRow, selector);
|
element = queryElement(currentRow, selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
record[label] = extractValue(element, attribute);
|
record[label] = extractValue(element, attribute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(record).length > 0) {
|
if (Object.keys(record).length > 0) {
|
||||||
tableData.push(record);
|
tableData.push(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process non-table data
|
// Process non-table data with both contexts support
|
||||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||||
if (nonTableData.length >= limit) break;
|
if (nonTableData.length >= limit) break;
|
||||||
|
|
||||||
const container = containers[containerIndex];
|
const container = containers[containerIndex];
|
||||||
const { nonTableFields } = containerFields[containerIndex];
|
const { nonTableFields } = containerFields[containerIndex];
|
||||||
|
|
||||||
if (Object.keys(nonTableFields).length > 0) {
|
if (Object.keys(nonTableFields).length > 0) {
|
||||||
const record = {};
|
const record = {};
|
||||||
|
|
||||||
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||||
const relativeSelector = selector.split(':>>').slice(-1)[0];
|
// Get the last part of the selector after any context delimiter
|
||||||
const element = queryIframe(container, relativeSelector);
|
const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
|
||||||
|
const element = queryElement(container, relativeSelector);
|
||||||
if (element) {
|
|
||||||
record[label] = extractValue(element, attribute);
|
if (element) {
|
||||||
}
|
record[label] = extractValue(element, attribute);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (Object.keys(record).length > 0) {
|
|
||||||
nonTableData.push(record);
|
if (Object.keys(record).length > 0) {
|
||||||
}
|
nonTableData.push(record);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge and limit the results
|
// Merge and limit the results
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ export default class Interpreter extends EventEmitter {
|
|||||||
await this.options.serializableCallback(scrapeResults);
|
await this.options.serializableCallback(scrapeResults);
|
||||||
},
|
},
|
||||||
|
|
||||||
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; }>) => {
|
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; shadow: string}>) => {
|
||||||
await this.ensureScriptsLoaded(page);
|
await this.ensureScriptsLoaded(page);
|
||||||
|
|
||||||
const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema);
|
const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema);
|
||||||
@@ -663,27 +663,42 @@ export default class Interpreter extends EventEmitter {
|
|||||||
if (isApplicable) {
|
if (isApplicable) {
|
||||||
return actionId;
|
return actionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeIframeSelectors(workflow: Workflow) {
|
private removeShadowSelectors(workflow: Workflow) {
|
||||||
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
||||||
const step = workflow[actionId];
|
const step = workflow[actionId];
|
||||||
|
|
||||||
// Check if step has where and selectors
|
// Check if step has where and selectors
|
||||||
if (step.where && Array.isArray(step.where.selectors)) {
|
if (step.where && Array.isArray(step.where.selectors)) {
|
||||||
// Filter out selectors that contain ">>"
|
// Filter out selectors that contain ">>"
|
||||||
step.where.selectors = step.where.selectors.filter(selector => !selector.includes(':>>'));
|
step.where.selectors = step.where.selectors.filter(selector => !selector.includes('>>'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeSpecialSelectors(workflow: Workflow) {
|
||||||
|
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
||||||
|
const step = workflow[actionId];
|
||||||
|
|
||||||
|
if (step.where && Array.isArray(step.where.selectors)) {
|
||||||
|
// Filter out if selector has EITHER ":>>" OR ">>"
|
||||||
|
step.where.selectors = step.where.selectors.filter(selector =>
|
||||||
|
!(selector.includes(':>>') || selector.includes('>>'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
private async runLoop(p: Page, workflow: Workflow) {
|
private async runLoop(p: Page, workflow: Workflow) {
|
||||||
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
||||||
|
|
||||||
workflowCopy = this.removeIframeSelectors(workflowCopy);
|
workflowCopy = this.removeSpecialSelectors(workflowCopy);
|
||||||
|
|
||||||
// apply ad-blocker to the current page
|
// apply ad-blocker to the current page
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -129,11 +129,17 @@ export interface BaseActionInfo {
|
|||||||
hasOnlyText: boolean;
|
hasOnlyText: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface IframeSelector {
|
interface IframeSelector {
|
||||||
full: string;
|
full: string;
|
||||||
isIframe: boolean;
|
isIframe: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShadowSelector {
|
||||||
|
full: string;
|
||||||
|
mode: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds all the possible css selectors that has been found for an element.
|
* Holds all the possible css selectors that has been found for an element.
|
||||||
* @category Types
|
* @category Types
|
||||||
@@ -149,6 +155,7 @@ export interface Selectors {
|
|||||||
accessibilitySelector: string|null;
|
accessibilitySelector: string|null;
|
||||||
formSelector: string|null;
|
formSelector: string|null;
|
||||||
iframeSelector: IframeSelector|null;
|
iframeSelector: IframeSelector|null;
|
||||||
|
shadowSelector: ShadowSelector|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -730,15 +730,26 @@ export class WorkflowGenerator {
|
|||||||
const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
|
const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
|
||||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
|
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
|
||||||
if (rect) {
|
if (rect) {
|
||||||
|
const highlighterData = {
|
||||||
|
rect,
|
||||||
|
selector: displaySelector,
|
||||||
|
elementInfo,
|
||||||
|
// Include shadow DOM specific information
|
||||||
|
shadowInfo: elementInfo?.isShadowRoot ? {
|
||||||
|
mode: elementInfo.shadowRootMode,
|
||||||
|
content: elementInfo.shadowRootContent
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
|
||||||
if (this.getList === true) {
|
if (this.getList === true) {
|
||||||
if (this.listSelector !== '') {
|
if (this.listSelector !== '') {
|
||||||
const childSelectors = await getChildSelectors(page, this.listSelector || '');
|
const childSelectors = await getChildSelectors(page, this.listSelector || '');
|
||||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors })
|
this.socket.emit('highlighter', { ...highlighterData, childSelectors })
|
||||||
} else {
|
} else {
|
||||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
|
this.socket.emit('highlighter', { ...highlighterData });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
|
this.socket.emit('highlighter', { ...highlighterData });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,12 @@ export const getBestSelectorForAction = (action: Action) => {
|
|||||||
case ActionType.DragAndDrop: {
|
case ActionType.DragAndDrop: {
|
||||||
const selectors = action.selectors;
|
const selectors = action.selectors;
|
||||||
|
|
||||||
|
|
||||||
if (selectors?.iframeSelector?.full) {
|
if (selectors?.iframeSelector?.full) {
|
||||||
return selectors.iframeSelector.full;
|
return selectors.iframeSelector.full;
|
||||||
|
|
||||||
|
if (selectors?.shadowSelector?.full) {
|
||||||
|
return selectors.shadowSelector.full;
|
||||||
}
|
}
|
||||||
|
|
||||||
// less than 25 characters, and element only has text inside
|
// less than 25 characters, and element only has text inside
|
||||||
@@ -80,6 +84,11 @@ export const getBestSelectorForAction = (action: Action) => {
|
|||||||
case ActionType.Input:
|
case ActionType.Input:
|
||||||
case ActionType.Keydown: {
|
case ActionType.Keydown: {
|
||||||
const selectors = action.selectors;
|
const selectors = action.selectors;
|
||||||
|
|
||||||
|
if (selectors?.shadowSelector?.full) {
|
||||||
|
return selectors.shadowSelector.full;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
selectors.testIdSelector ??
|
selectors.testIdSelector ??
|
||||||
selectors?.id ??
|
selectors?.id ??
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { useBrowserSteps, TextStep } from '../../context/browserSteps';
|
|||||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
interface ElementInfo {
|
interface ElementInfo {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
hasOnlyText?: boolean;
|
hasOnlyText?: boolean;
|
||||||
isIframeContent?: boolean;
|
isIframeContent?: boolean;
|
||||||
|
isShadowRoot?: boolean;
|
||||||
innerText?: string;
|
innerText?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
@@ -125,7 +125,7 @@ export const BrowserWindow = () => {
|
|||||||
if (listSelector) {
|
if (listSelector) {
|
||||||
socket?.emit('listSelector', { selector: listSelector });
|
socket?.emit('listSelector', { selector: listSelector });
|
||||||
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
|
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
|
||||||
|
|
||||||
if (limitMode) {
|
if (limitMode) {
|
||||||
setHighlighterData(null);
|
setHighlighterData(null);
|
||||||
} else if (paginationMode) {
|
} else if (paginationMode) {
|
||||||
@@ -159,18 +159,39 @@ export const BrowserWindow = () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
setHighlighterData(isValidMixedSelector ? data : null);
|
setHighlighterData(isValidMixedSelector ? data : null);
|
||||||
} else {
|
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
|
||||||
// If no valid child in normal mode, clear the highlighter
|
// New case: Handle pure Shadow DOM elements
|
||||||
|
// Check if the selector matches any shadow root child selectors
|
||||||
|
const isShadowChild = data.childSelectors.some(childSelector =>
|
||||||
|
data.selector.includes('>>') && // Shadow DOM uses >> for piercing
|
||||||
|
childSelector.split('>>').some(part =>
|
||||||
|
data.selector.includes(part.trim())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setHighlighterData(isShadowChild ? data : null);
|
||||||
|
} else if (data.selector.includes('>>') && hasValidChildSelectors) {
|
||||||
|
// New case: Handle mixed DOM cases
|
||||||
|
// Split the selector into parts and check each against child selectors
|
||||||
|
const selectorParts = data.selector.split('>>').map(part => part.trim());
|
||||||
|
const isValidMixedSelector = selectorParts.some(part =>
|
||||||
|
// Now we know data.childSelectors is defined
|
||||||
|
data.childSelectors!.some(childSelector =>
|
||||||
|
childSelector.includes(part)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setHighlighterData(isValidMixedSelector ? data : null);
|
||||||
|
} else {
|
||||||
|
// if !valid child in normal mode, clear the highlighter
|
||||||
setHighlighterData(null);
|
setHighlighterData(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Set highlighterData for the initial listSelector selection
|
// Set highlighterData for the initial listSelector selection
|
||||||
setHighlighterData(data);
|
setHighlighterData(data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For non-list steps
|
// For non-list steps
|
||||||
setHighlighterData(data);
|
setHighlighterData(data);
|
||||||
}
|
}
|
||||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
|
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
|
||||||
|
|
||||||
|
|
||||||
@@ -219,6 +240,7 @@ export const BrowserWindow = () => {
|
|||||||
addTextStep('', data, {
|
addTextStep('', data, {
|
||||||
selector: highlighterData.selector,
|
selector: highlighterData.selector,
|
||||||
tag: highlighterData.elementInfo?.tagName,
|
tag: highlighterData.elementInfo?.tagName,
|
||||||
|
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||||
attribute
|
attribute
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -226,7 +248,7 @@ export const BrowserWindow = () => {
|
|||||||
setAttributeOptions(options);
|
setAttributeOptions(options);
|
||||||
setSelectedElement({
|
setSelectedElement({
|
||||||
selector: highlighterData.selector,
|
selector: highlighterData.selector,
|
||||||
info: highlighterData.elementInfo
|
info: highlighterData.elementInfo,
|
||||||
});
|
});
|
||||||
setShowAttributeModal(true);
|
setShowAttributeModal(true);
|
||||||
}
|
}
|
||||||
@@ -263,6 +285,7 @@ export const BrowserWindow = () => {
|
|||||||
selectorObj: {
|
selectorObj: {
|
||||||
selector: highlighterData.selector,
|
selector: highlighterData.selector,
|
||||||
tag: highlighterData.elementInfo?.tagName,
|
tag: highlighterData.elementInfo?.tagName,
|
||||||
|
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||||
attribute
|
attribute
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -310,6 +333,7 @@ export const BrowserWindow = () => {
|
|||||||
addTextStep('', data, {
|
addTextStep('', data, {
|
||||||
selector: selectedElement.selector,
|
selector: selectedElement.selector,
|
||||||
tag: selectedElement.info?.tagName,
|
tag: selectedElement.info?.tagName,
|
||||||
|
shadow: selectedElement.info?.isShadowRoot,
|
||||||
attribute: attribute
|
attribute: attribute
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -322,6 +346,7 @@ export const BrowserWindow = () => {
|
|||||||
selectorObj: {
|
selectorObj: {
|
||||||
selector: selectedElement.selector,
|
selector: selectedElement.selector,
|
||||||
tag: selectedElement.info?.tagName,
|
tag: selectedElement.info?.tagName,
|
||||||
|
shadow: selectedElement.info?.isShadowRoot,
|
||||||
attribute: attribute
|
attribute: attribute
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface SelectorObject {
|
|||||||
selector: string;
|
selector: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
shadow?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user