firefox-keyword-grab/content-script.js
2024-11-21 15:03:05 -05:00

862 lines
30 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 per page');
return true;
}
// First find the dropdown container
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'
];
let dropdownButton = null;
for (const selector of dropdownSelectors) {
const element = document.querySelector(selector);
if (element) {
console.log(`Found dropdown using selector: ${selector}`);
dropdownButton = element;
break;
}
}
if (!dropdownButton) {
console.log('Could not find rows dropdown button');
return false;
}
// Click the dropdown to open it
console.log('Clicking dropdown button...');
dropdownButton.click();
await new Promise(resolve => setTimeout(resolve, 2000));
// Look for the 500 option
const optionSelectors = [
'material-select-dropdown-item',
'[role="option"]'
];
let found500Option = null;
for (const selector of optionSelectors) {
const options = Array.from(document.querySelectorAll(selector));
for (const option of options) {
const text = option.textContent.trim();
if (text === '500') {
found500Option = option;
console.log(`Found 500 option using selector: ${selector}`);
break;
}
}
if (found500Option) break;
}
if (found500Option) {
console.log('Clicking 500 option...');
found500Option.click();
// Wait for the page to update
await new Promise(resolve => setTimeout(resolve, 3000));
// Verify the change by checking page info
const updatedInfo = getCurrentPageInfo();
if (updatedInfo && updatedInfo.perPage === 500) {
console.log('Successfully changed to 500 rows');
return true;
}
}
console.log('Could not find or click 500 option');
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'));
}
});
})();