911 lines
31 KiB
JavaScript
911 lines
31 KiB
JavaScript
(function() {
|
|
// Don't start automatically, wait for custom event
|
|
let hasStarted = false;
|
|
let isExtracting = false;
|
|
|
|
// Listen for the start event
|
|
document.addEventListener('startKeywordExtraction', function() {
|
|
if (!hasStarted && !isExtracting) {
|
|
hasStarted = true;
|
|
waitForContent();
|
|
}
|
|
});
|
|
|
|
// Helper function to wait for an element to be present and visible
|
|
function waitForElement(selector, timeout = 10000) {
|
|
return new Promise((resolve, reject) => {
|
|
const startTime = Date.now();
|
|
|
|
function checkElement() {
|
|
const element = document.querySelector(selector);
|
|
if (element && element.offsetParent !== null) { // Check if element is visible
|
|
resolve(element);
|
|
} else if (Date.now() - startTime > timeout) {
|
|
reject(new Error(`Timeout waiting for element: ${selector}`));
|
|
} else {
|
|
setTimeout(checkElement, 100);
|
|
}
|
|
}
|
|
|
|
checkElement();
|
|
});
|
|
}
|
|
|
|
// Helper function to get current row count
|
|
function getRowCount() {
|
|
const rows = document.querySelectorAll('.particle-table-row');
|
|
return rows.length;
|
|
}
|
|
|
|
// Helper function to wait for page update
|
|
async function waitForPageUpdate(previousStart) {
|
|
console.log('Waiting for page update...');
|
|
|
|
// Initial wait for page transition
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Check for page update
|
|
for (let i = 0; i < 10; i++) {
|
|
const pageInfo = getCurrentPageInfo();
|
|
if (!pageInfo) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
continue;
|
|
}
|
|
|
|
if (pageInfo.start !== previousStart) {
|
|
console.log(`Page updated: ${previousStart} -> ${pageInfo.start}`);
|
|
return true;
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Helper function to load all rows by scrolling
|
|
async function loadAllRows() {
|
|
console.log('Waiting for all rows to load...');
|
|
|
|
const container = document.querySelector('.main-container');
|
|
if (!container) {
|
|
console.log('Could not find scrollable container');
|
|
return false;
|
|
}
|
|
|
|
// Scroll to bottom and wait
|
|
container.scrollTop = container.scrollHeight;
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
let lastRowCount = 0;
|
|
let currentRowCount = getRowCount();
|
|
let attempts = 0;
|
|
let maxAttempts = 3;
|
|
|
|
while (lastRowCount !== currentRowCount && attempts < maxAttempts) {
|
|
lastRowCount = currentRowCount;
|
|
container.scrollTop = container.scrollHeight;
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
currentRowCount = getRowCount();
|
|
attempts++;
|
|
console.log(`Row loading attempt ${attempts}: ${currentRowCount} rows`);
|
|
}
|
|
|
|
// If we have rows and they're stable (not changing), consider it successful
|
|
return currentRowCount > 0;
|
|
}
|
|
|
|
// Helper function to wait for content to load after navigation
|
|
async function waitForContentLoad() {
|
|
console.log('Waiting for content to load...');
|
|
|
|
// First wait for any loading indicators to disappear
|
|
await delay(2000);
|
|
|
|
// Try to force content load by scrolling multiple times
|
|
const container = document.querySelector('.main-container');
|
|
if (container) {
|
|
for (let i = 0; i < 3; i++) {
|
|
console.log(`Scroll attempt ${i + 1}/3 to force content load`);
|
|
// Scroll to different positions
|
|
container.scrollTo(0, container.scrollHeight * 0.3);
|
|
await delay(500);
|
|
container.scrollTo(0, container.scrollHeight * 0.6);
|
|
await delay(500);
|
|
container.scrollTo(0, container.scrollHeight);
|
|
await delay(1000);
|
|
}
|
|
}
|
|
|
|
// Then wait for all rows to load
|
|
const success = await loadAllRows();
|
|
if (!success) {
|
|
console.log('Warning: Could not verify all rows loaded after navigation');
|
|
}
|
|
|
|
// Final wait to ensure everything is stable
|
|
await delay(1000);
|
|
|
|
// Verify we have content
|
|
const rows = document.querySelectorAll('.particle-table-row');
|
|
console.log(`Found ${rows.length} rows after content load`);
|
|
|
|
return rows.length > 0;
|
|
}
|
|
|
|
// Helper function to ensure content is visible
|
|
async function ensureContentVisible() {
|
|
const container = document.querySelector('.main-container');
|
|
if (!container) return;
|
|
|
|
// Scroll to bottom to ensure all content is loaded
|
|
console.log('Scrolling to bottom to ensure content is loaded...');
|
|
container.scrollTo(0, container.scrollHeight);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Scroll back to top
|
|
container.scrollTo(0, 0);
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
// Helper function to get current page info
|
|
function getCurrentPageInfo() {
|
|
try {
|
|
// Try multiple selectors to find the pagination text
|
|
const selectors = [
|
|
'.wrap-content > div.selected > div', // Main selector
|
|
'.wrap-content div:nth-child(1) > div:nth-child(1)', // Alternative selector
|
|
'.selected > div' // Fallback selector
|
|
];
|
|
|
|
let paginationText = null;
|
|
for (const selector of selectors) {
|
|
const element = document.querySelector(selector);
|
|
if (element?.textContent) {
|
|
paginationText = element.textContent.trim();
|
|
console.log(`Found pagination text using selector "${selector}": "${paginationText}"`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!paginationText) {
|
|
// Check if we have enough rows to determine if pagination should exist
|
|
const rows = document.querySelectorAll('table tbody tr');
|
|
if (rows.length < 500) {
|
|
console.log(`Only ${rows.length} rows found, assuming single page`);
|
|
return {
|
|
start: 1,
|
|
end: rows.length,
|
|
total: rows.length,
|
|
perPage: rows.length
|
|
};
|
|
}
|
|
console.log('No pagination text found');
|
|
return null;
|
|
}
|
|
|
|
// Handle various pagination text formats
|
|
const match = paginationText.match(/(\d+[\d,]*)\s*-\s*(\d+[\d,]*)\s+of\s+(\d+[\d,]*)/);
|
|
if (!match) {
|
|
console.log('Could not parse pagination text:', paginationText);
|
|
return null;
|
|
}
|
|
|
|
// Remove commas from numbers before parsing
|
|
const start = parseInt(match[1].replace(/,/g, ''));
|
|
const end = parseInt(match[2].replace(/,/g, ''));
|
|
const total = parseInt(match[3].replace(/,/g, ''));
|
|
|
|
return {
|
|
start,
|
|
end,
|
|
total,
|
|
perPage: end - start + 1
|
|
};
|
|
} catch (error) {
|
|
console.error('Error getting page info:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Helper function to wait for paginator to be ready
|
|
async function waitForPaginatorReady(maxAttempts = 10) {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
// Check for paginator container
|
|
const paginator = document.querySelector('.mat-mdc-paginator-container, .mat-paginator-container');
|
|
if (!paginator) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
continue;
|
|
}
|
|
|
|
// Check if paginator is interactive
|
|
const buttons = paginator.querySelectorAll('button');
|
|
let allDisabled = true;
|
|
for (const button of buttons) {
|
|
if (!button.hasAttribute('disabled')) {
|
|
allDisabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!allDisabled) {
|
|
return true;
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Helper function to check if we're on the last page
|
|
function isLastPage() {
|
|
const pageInfo = getCurrentPageInfo();
|
|
if (!pageInfo) return true;
|
|
|
|
// Check if we're at the end based on current range
|
|
const lastItemOnPage = pageInfo.start + pageInfo.perPage - 1;
|
|
if (lastItemOnPage >= pageInfo.total) {
|
|
console.log('Reached last item, on last page');
|
|
return true;
|
|
}
|
|
|
|
const nextButton = findNextButton();
|
|
if (!nextButton) {
|
|
console.log('No next button found, assuming last page');
|
|
return true;
|
|
}
|
|
|
|
// Check various disabled states
|
|
const isDisabled = nextButton.hasAttribute('disabled') ||
|
|
nextButton.getAttribute('aria-disabled') === 'true' ||
|
|
nextButton.classList.contains('disabled') ||
|
|
nextButton.closest('[aria-disabled="true"]') !== null ||
|
|
window.getComputedStyle(nextButton).opacity === '0.5';
|
|
|
|
if (isDisabled) {
|
|
console.log('Next button is disabled, on last page');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Helper function to find next button
|
|
function findNextButton() {
|
|
const nextButtonSelectors = [
|
|
// Specific selector for this case
|
|
'material-button.next[aria-label="Go to the next page"]',
|
|
'.next[role="button"]:not([aria-disabled="true"])',
|
|
'material-button.next:not([aria-disabled="true"])',
|
|
// Generic next page selectors
|
|
'material-button[aria-label="Go to the next page"]:not([aria-disabled="true"])',
|
|
'[aria-label="Go to the next page"]:not([aria-disabled="true"])',
|
|
// Fallback selectors
|
|
'.next:not([aria-disabled="true"])',
|
|
'material-button.next'
|
|
];
|
|
|
|
for (const selector of nextButtonSelectors) {
|
|
const buttons = document.querySelectorAll(selector);
|
|
for (const button of buttons) {
|
|
// Check if button exists and is connected to DOM
|
|
if (!button || !button.isConnected) continue;
|
|
|
|
// Check computed style
|
|
const style = window.getComputedStyle(button);
|
|
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
|
|
// Check if button is in viewport and clickable
|
|
const rect = button.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) continue;
|
|
|
|
// Check if button is truly enabled
|
|
if (button.getAttribute('aria-disabled') === 'true' ||
|
|
button.hasAttribute('disabled') ||
|
|
button.classList.contains('disabled')) {
|
|
console.log('Next button found but is disabled');
|
|
continue;
|
|
}
|
|
|
|
console.log(`Found next button using selector: ${selector}`);
|
|
return button;
|
|
}
|
|
}
|
|
|
|
console.log('Could not find next button');
|
|
return null;
|
|
}
|
|
|
|
// Helper function to go to next page
|
|
async function goToNextPage() {
|
|
const nextButton = findNextButton();
|
|
|
|
if (!nextButton) {
|
|
console.log('Could not find next page button');
|
|
return false;
|
|
}
|
|
|
|
if (nextButton.getAttribute('aria-disabled') === 'true') {
|
|
console.log('Next button is disabled - on last page');
|
|
return false;
|
|
}
|
|
|
|
// Ensure content is visible before clicking next
|
|
await ensureContentVisible();
|
|
|
|
const previousStart = getCurrentPageInfo()?.start;
|
|
if (!previousStart) {
|
|
console.log('Could not determine current page position');
|
|
return false;
|
|
}
|
|
|
|
console.log('Clicking next page button...');
|
|
nextButton.click();
|
|
|
|
// Wait for the page to update
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
const updated = await waitForPageUpdate(previousStart);
|
|
|
|
if (!updated) {
|
|
console.log('Page did not update after clicking next');
|
|
return false;
|
|
}
|
|
|
|
// Wait for content to load after page change
|
|
await waitForContentLoad();
|
|
console.log('Successfully moved to next page');
|
|
return true;
|
|
}
|
|
|
|
// Helper function to extract current page
|
|
async function extractCurrentPage() {
|
|
// Wait for rows to be loaded
|
|
if (!await waitForRowsToLoad()) {
|
|
console.log('Failed to load rows, but checking if we have any content');
|
|
// Even if waitForRowsToLoad fails, check if we have any rows
|
|
const rows = document.querySelectorAll('.particle-table-row');
|
|
if (rows.length === 0) {
|
|
createNotification('No keywords found on this page', 3000, true);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Find all keyword sections
|
|
const rows = document.querySelectorAll('.particle-table-row');
|
|
console.log(`Found ${rows.length} rows to process`);
|
|
|
|
const keywords = [];
|
|
rows.forEach(row => {
|
|
try {
|
|
// Get keyword text
|
|
const keywordElement = row.querySelector('.keyword-text');
|
|
const volumeElement = row.querySelector('sparkline-graph .value-text');
|
|
|
|
if (keywordElement && volumeElement) {
|
|
const keyword = keywordElement.textContent.trim();
|
|
const volume = volumeElement.textContent.trim();
|
|
|
|
// Skip keywords with "—" volume
|
|
if (volume === '—') {
|
|
return;
|
|
}
|
|
|
|
// Find the closest group header
|
|
let currentRow = row;
|
|
let source = 'Keyword Ideas'; // default
|
|
while (currentRow) {
|
|
const header = currentRow.previousElementSibling;
|
|
if (header && header.classList.contains('group-header')) {
|
|
const headerText = header.textContent.trim();
|
|
if (headerText.includes('Keywords you provided')) {
|
|
source = 'Provided Keywords';
|
|
}
|
|
break;
|
|
}
|
|
currentRow = currentRow.previousElementSibling;
|
|
}
|
|
|
|
keywords.push({
|
|
keyword,
|
|
volume: volume.replace(/,/g, ''),
|
|
source
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing row:', error);
|
|
}
|
|
});
|
|
|
|
return keywords;
|
|
}
|
|
|
|
// Helper function to create notifications
|
|
function createNotification(message, duration = 3000, isError = false, id = null, remove = false) {
|
|
if (remove && id) {
|
|
const existingNotification = document.getElementById(id);
|
|
if (existingNotification) {
|
|
existingNotification.remove();
|
|
}
|
|
return id;
|
|
}
|
|
|
|
// If updating existing notification
|
|
if (id) {
|
|
const existingNotification = document.getElementById(id);
|
|
if (existingNotification) {
|
|
existingNotification.textContent = message;
|
|
return id;
|
|
}
|
|
}
|
|
|
|
// Create new notification
|
|
const notification = document.createElement('div');
|
|
const notificationId = id || 'notification-' + Date.now();
|
|
notification.id = notificationId;
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 10px 20px;
|
|
background-color: ${isError ? '#ff4444' : '#4CAF50'};
|
|
color: white;
|
|
border-radius: 4px;
|
|
z-index: 10000;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
max-width: 300px;
|
|
word-wrap: break-word;
|
|
`;
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
const notificationToRemove = document.getElementById(notificationId);
|
|
if (notificationToRemove) {
|
|
notificationToRemove.remove();
|
|
}
|
|
}, duration);
|
|
}
|
|
|
|
return notificationId;
|
|
}
|
|
|
|
// Helper function to set show rows to 500
|
|
async function setShowRows500() {
|
|
try {
|
|
// First check if we're already at 500 rows
|
|
const currentInfo = getCurrentPageInfo();
|
|
if (currentInfo && currentInfo.perPage === 500) {
|
|
console.log('Already at 500 rows');
|
|
return true;
|
|
}
|
|
|
|
// Find the dropdown button using multiple selectors
|
|
const dropdownSelectors = [
|
|
'material-dropdown-select[aria-label="Show rows"]',
|
|
'material-dropdown-select[aria-label="Show rows"] material-dropdown-select',
|
|
'[aria-label="Show rows: 100 selected."]',
|
|
'.dropdown-label-container',
|
|
'div[aria-label^="Show rows:"]',
|
|
'material-dropdown-select[role="listbox"]'
|
|
];
|
|
|
|
let dropdownButton = null;
|
|
for (const selector of dropdownSelectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
for (const element of elements) {
|
|
if (element.offsetParent !== null) {
|
|
console.log(`Found visible dropdown using selector: ${selector}`);
|
|
dropdownButton = element;
|
|
break;
|
|
}
|
|
}
|
|
if (dropdownButton) break;
|
|
}
|
|
|
|
if (!dropdownButton) {
|
|
console.log('Could not find dropdown button');
|
|
return false;
|
|
}
|
|
|
|
// Click the dropdown to open it
|
|
console.log('Clicking dropdown button...');
|
|
dropdownButton.click();
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Wait for the popup to be visible
|
|
const popup = document.querySelector('.material-dropdown-select-popup.visible');
|
|
if (!popup) {
|
|
console.log('Dropdown popup not visible after click');
|
|
return false;
|
|
}
|
|
|
|
// Find the 500 option using multiple strategies
|
|
const option500Selectors = [
|
|
'material-select-dropdown-item[role="option"] .label',
|
|
'.item[role="option"] .label',
|
|
'material-select-dropdown-item .label'
|
|
];
|
|
|
|
let found500Option = null;
|
|
for (const selector of option500Selectors) {
|
|
const options = Array.from(document.querySelectorAll(selector));
|
|
for (const option of options) {
|
|
if (option.textContent.trim() === '500') {
|
|
// Get the parent option element
|
|
found500Option = option.closest('[role="option"]');
|
|
if (found500Option?.offsetParent !== null) {
|
|
console.log(`Found visible 500 option using selector: ${selector}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (found500Option) break;
|
|
}
|
|
|
|
if (!found500Option) {
|
|
// Try finding by aria-label as fallback
|
|
const ariaOptions = document.querySelectorAll('[aria-label*="500"]');
|
|
for (const option of ariaOptions) {
|
|
if (option.offsetParent !== null) {
|
|
found500Option = option;
|
|
console.log('Found 500 option using aria-label');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found500Option) {
|
|
console.log('Could not find 500 option in dropdown');
|
|
// Close the dropdown since we couldn't find the option
|
|
document.body.click();
|
|
return false;
|
|
}
|
|
|
|
// Click the 500 option
|
|
console.log('Clicking 500 option...');
|
|
found500Option.click();
|
|
|
|
// Wait for the change to take effect
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Verify the change
|
|
const updatedInfo = getCurrentPageInfo();
|
|
if (updatedInfo && updatedInfo.perPage === 500) {
|
|
console.log('Successfully changed to 500 rows');
|
|
return true;
|
|
}
|
|
|
|
// Additional verification using pagination text
|
|
const paginationText = await getPaginationText();
|
|
if (paginationText && typeof paginationText === 'string') {
|
|
console.log('Current pagination text:', paginationText);
|
|
|
|
// Check if we're seeing more than 100 rows
|
|
const match = paginationText.match(/(\d+)\s*-\s*(\d+)/);
|
|
if (match) {
|
|
const [, start, end] = match;
|
|
if (parseInt(end) - parseInt(start) > 99) {
|
|
console.log('Pagination shows more than 100 rows, considering success');
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Could not verify row count change');
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error setting rows to 500:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Helper function to check if we should handle pagination
|
|
function shouldHandlePagination(options) {
|
|
// Always return true since we need to set rows to 500 regardless of extractAllPages
|
|
return true;
|
|
}
|
|
|
|
// Helper function to handle pagination
|
|
async function handlePagination() {
|
|
// Initial scroll to ensure current page is fully loaded
|
|
await scrollToBottom();
|
|
await waitForRowsToLoad();
|
|
|
|
// Get current page info
|
|
const paginationText = await getPaginationText();
|
|
if (!paginationText) {
|
|
console.log('No pagination text found');
|
|
return false;
|
|
}
|
|
|
|
// Find and click next button
|
|
const nextButton = findNextButton();
|
|
if (!nextButton) {
|
|
console.log('No next button found, assuming last page');
|
|
return false;
|
|
}
|
|
|
|
// Get current info before moving
|
|
const currentInfo = getCurrentPageInfo();
|
|
if (!currentInfo) {
|
|
console.log('Could not get current page info');
|
|
return false;
|
|
}
|
|
|
|
// Click the next button
|
|
console.log('Clicking next button...');
|
|
nextButton.click();
|
|
|
|
// Wait for the page to update
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
const updated = await waitForPageUpdate(currentInfo.start);
|
|
|
|
if (!updated) {
|
|
console.log('Page did not update after clicking next');
|
|
return false;
|
|
}
|
|
|
|
// Wait for content to load after page change
|
|
await waitForContentLoad();
|
|
console.log('Successfully moved to next page');
|
|
return true;
|
|
}
|
|
|
|
// Helper function to create a delay
|
|
function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Helper function to scroll to bottom
|
|
async function scrollToBottom() {
|
|
const container = document.querySelector('.main-container');
|
|
if (!container) return;
|
|
|
|
// Scroll to bottom to ensure all content is loaded
|
|
console.log('Scrolling to bottom to ensure content is loaded...');
|
|
container.scrollTo(0, container.scrollHeight);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// Helper function to wait for rows to load
|
|
async function waitForRowsToLoad() {
|
|
// Function to get current row count
|
|
const getRowCount = () => {
|
|
const rows = document.querySelectorAll('.particle-table-row');
|
|
return rows.length;
|
|
};
|
|
|
|
let lastRowCount = 0;
|
|
let sameCountIterations = 0;
|
|
let maxAttempts = 5;
|
|
let attempts = 0;
|
|
|
|
while (attempts < maxAttempts) {
|
|
const currentRowCount = getRowCount();
|
|
console.log('Current row count:', currentRowCount, sameCountIterations);
|
|
|
|
if (currentRowCount === lastRowCount) {
|
|
sameCountIterations++;
|
|
if (sameCountIterations >= 2 && currentRowCount > 0) {
|
|
console.log('Row loading complete. Total rows:', currentRowCount);
|
|
return true;
|
|
}
|
|
} else {
|
|
sameCountIterations = 0;
|
|
}
|
|
|
|
lastRowCount = currentRowCount;
|
|
|
|
// Scroll to bottom
|
|
const container = document.querySelector('.main-container');
|
|
if (container) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
attempts++;
|
|
}
|
|
|
|
// If we have rows but didn't meet the stability criteria, still consider it a success
|
|
const finalRowCount = getRowCount();
|
|
if (finalRowCount > 0) {
|
|
console.log('Row loading completed with', finalRowCount, 'rows');
|
|
return true;
|
|
}
|
|
|
|
console.log('Could not load any rows after maximum attempts');
|
|
return false;
|
|
}
|
|
|
|
// Helper function to get pagination text
|
|
async function getPaginationText() {
|
|
// Try multiple selectors to find the pagination text
|
|
const selectors = [
|
|
'.wrap-content > div.selected > div', // Main selector
|
|
'.wrap-content div:nth-child(1) > div:nth-child(1)', // Alternative selector
|
|
'.selected > div' // Fallback selector
|
|
];
|
|
|
|
let paginationText = null;
|
|
for (const selector of selectors) {
|
|
const element = document.querySelector(selector);
|
|
if (element?.textContent) {
|
|
paginationText = element.textContent.trim();
|
|
console.log(`Found pagination text using selector "${selector}": "${paginationText}"`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return paginationText;
|
|
}
|
|
|
|
// Helper function to format keywords into CSV format
|
|
function formatKeywords(keywords, includeHeaders) {
|
|
let csvContent = '';
|
|
if (includeHeaders) {
|
|
csvContent = 'Keyword,Search Volume,Source\n';
|
|
}
|
|
csvContent += keywords.map(k => `${k.keyword},${k.volume},${k.source}`).join('\n');
|
|
return csvContent;
|
|
}
|
|
|
|
// Main function to extract keywords
|
|
async function extractKeywords() {
|
|
if (isExtracting) {
|
|
console.log('Already extracting keywords');
|
|
return;
|
|
}
|
|
|
|
isExtracting = true;
|
|
let progressNotificationId = null;
|
|
try {
|
|
// Initial setup
|
|
await waitForContent();
|
|
|
|
// Get initial page info to check total keywords
|
|
const initialPageInfo = getCurrentPageInfo();
|
|
if (!initialPageInfo) {
|
|
throw new Error('Could not get page information');
|
|
}
|
|
|
|
console.log(`Total keywords found: ${initialPageInfo.total}`);
|
|
|
|
// Only modify show rows if we have more than 100 keywords
|
|
if (initialPageInfo.total > 100) {
|
|
console.log('More than 100 keywords found, setting show rows to 500');
|
|
if (!await setShowRows500()) {
|
|
throw new Error('Failed to set show rows to 500');
|
|
}
|
|
await waitForContent();
|
|
} else {
|
|
console.log('Less than 100 keywords found, skipping show rows modification');
|
|
}
|
|
|
|
// Get updated page info after potential show rows change
|
|
const pageInfo = getCurrentPageInfo();
|
|
if (!pageInfo) {
|
|
throw new Error('Could not get page information');
|
|
}
|
|
|
|
console.log(`Starting extraction from page ${pageInfo.start} to ${pageInfo.end} of ${pageInfo.total}`);
|
|
|
|
const allKeywords = [];
|
|
let currentPage = 1;
|
|
|
|
// Create initial progress notification
|
|
progressNotificationId = createNotification(`Extracting keywords... (0/${pageInfo.total})`, 0, false, 'progress-notification');
|
|
|
|
do {
|
|
console.log(`Processing page ${currentPage}`);
|
|
|
|
const keywords = await extractCurrentPage();
|
|
if (!keywords) {
|
|
throw new Error('Failed to extract keywords from current page');
|
|
}
|
|
|
|
allKeywords.push(...keywords);
|
|
console.log(`Total keywords collected: ${allKeywords.length}`);
|
|
|
|
// Update progress notification
|
|
createNotification(`Extracting keywords... (${allKeywords.length}/${pageInfo.total})`, 0, false, progressNotificationId);
|
|
|
|
if (await isLastPage()) {
|
|
break;
|
|
}
|
|
|
|
if (!await goToNextPage()) {
|
|
throw new Error('Failed to navigate to next page');
|
|
}
|
|
|
|
currentPage++;
|
|
|
|
} while (true);
|
|
|
|
if (allKeywords.length === 0) {
|
|
throw new Error('No keywords found');
|
|
}
|
|
|
|
// Format the keywords and copy to clipboard
|
|
const formattedKeywords = formatKeywords(allKeywords, window.keywordGrabberOptions?.includeHeaders ?? true);
|
|
await navigator.clipboard.writeText(formattedKeywords);
|
|
|
|
console.log(`Successfully extracted ${allKeywords.length} keywords`);
|
|
|
|
// Remove progress notification before showing success
|
|
createNotification('', 0, false, progressNotificationId, true);
|
|
createNotification(`Extracted ${allKeywords.length} keywords`);
|
|
|
|
return allKeywords;
|
|
|
|
} catch (error) {
|
|
console.error('Error during keyword extraction:', error);
|
|
// Remove progress notification before showing error
|
|
if (progressNotificationId) {
|
|
createNotification('', 0, false, progressNotificationId, true);
|
|
}
|
|
createNotification(error.message, 5000, true);
|
|
throw error;
|
|
} finally {
|
|
isExtracting = false;
|
|
hasStarted = false;
|
|
// Ensure progress notification is removed
|
|
if (progressNotificationId) {
|
|
createNotification('', 0, false, progressNotificationId, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Since this is an Angular app, content might load dynamically
|
|
// Let's try to wait for the content to load
|
|
function waitForContent() {
|
|
const maxAttempts = 20;
|
|
let attempts = 0;
|
|
|
|
async function tryFindContent() {
|
|
if (!hasStarted) {
|
|
console.log('Extraction cancelled');
|
|
return;
|
|
}
|
|
|
|
console.log('Attempt', attempts + 1, 'to find keyword content...');
|
|
|
|
// Look for the keyword grid and at least one row
|
|
const hasContent = document.querySelector('[role="grid"][aria-label="Keyword ideas"]') &&
|
|
document.querySelector('.particle-table-row');
|
|
|
|
if (hasContent) {
|
|
console.log('Content found, proceeding with row loading...');
|
|
// First ensure all rows are loaded, then extract
|
|
const success = await loadAllRows();
|
|
if (success && hasStarted) {
|
|
await extractKeywords();
|
|
} else if (hasStarted) { // Only show error if we haven't already completed
|
|
createNotification('Could not load all rows. Please try again.', 5000, true);
|
|
hasStarted = false;
|
|
}
|
|
} else if (attempts < maxAttempts && hasStarted) {
|
|
attempts++;
|
|
setTimeout(tryFindContent, 1000); // Wait 1 second before trying again
|
|
} else if (hasStarted) { // Only show error if we haven't already completed
|
|
createNotification('Could not find keyword content after multiple attempts. Please make sure you are on the correct page.', 5000, true);
|
|
hasStarted = false;
|
|
}
|
|
}
|
|
|
|
tryFindContent();
|
|
}
|
|
|
|
// Listen for messages from the popup
|
|
browser.runtime.onMessage.addListener((message) => {
|
|
if (message.action === "GrabKeywords" && !isExtracting) {
|
|
window.keywordGrabberOptions = {
|
|
includeHeaders: message.includeHeaders,
|
|
extractAllPages: true // Always extract all pages
|
|
};
|
|
// Reset state and trigger start
|
|
hasStarted = false;
|
|
// Trigger the start via a custom event
|
|
document.dispatchEvent(new CustomEvent('startKeywordExtraction'));
|
|
}
|
|
});
|
|
})(); |