First commit.

This commit is contained in:
Lord_Devi 2024-11-21 03:51:48 -05:00
commit 084ce6d6d6
6 changed files with 293 additions and 0 deletions

143
content-script.js Normal file
View file

@ -0,0 +1,143 @@
(function() {
function extractKeywords() {
// Find the keyword grid
const keywordGrid = document.querySelector('[role="grid"][aria-label="Keyword ideas"]');
if (!keywordGrid) {
alert('Could not find the keyword ideas grid. Please make sure you are on the correct page.');
return;
}
// Initialize CSV content based on includeHeaders preference
let csvContent = window.includeHeaders ? 'Keyword,Average Monthly Searches,Source\r\n' : '';
// Keep track of processed keywords to prevent duplicates
const processedKeywords = new Set();
// Function to clean search volume
function cleanSearchVolume(volume) {
// Remove any commas from the number
return volume.replace(/,/g, '');
}
// Function to process rows between start and end indices
function processRowsInRange(startIdx, endIdx, source) {
const rows = document.querySelectorAll('.particle-table-row');
for (let i = startIdx; i < Math.min(endIdx, rows.length); i++) {
const row = rows[i];
const keywordElement = row.querySelector('ess-cell keyword-text');
const searchVolumeElement = row.querySelector('ess-cell sparkline-graph span');
if (keywordElement && searchVolumeElement) {
const keyword = keywordElement.textContent.trim();
const searchVolume = cleanSearchVolume(searchVolumeElement.textContent.trim());
// Only add if we have both values, it's not a header, and we haven't processed this keyword yet
if (keyword &&
!keyword.includes('Keyword') &&
!keyword.includes('Add to plan') &&
!processedKeywords.has(keyword)) {
// Add to processed set
processedKeywords.add(keyword);
// Properly escape the keyword if it contains commas
const escapedKeyword = keyword.includes(',') ? `"${keyword}"` : keyword;
csvContent += `${escapedKeyword},${searchVolume},"${source}"\r\n`;
}
}
}
}
// Find all section headers and their positions
const rows = Array.from(document.querySelectorAll('.particle-table-row'));
const sections = [];
// Find all sections and their boundaries
rows.forEach((row, index) => {
const cellText = row.querySelector('ess-cell')?.textContent?.trim();
if (cellText === 'Keyword ideas' || cellText === 'Keywords you provided') {
sections.push({
type: cellText,
start: index + 1, // Start after header
end: rows.length // Will be updated for all except last section
});
}
});
// Update section end boundaries
for (let i = 0; i < sections.length - 1; i++) {
sections[i].end = sections[i + 1].start - 1;
}
// Process each section
sections.forEach(section => {
const source = section.type === 'Keyword ideas' ? 'Keyword Ideas' : 'Provided Keywords';
processRowsInRange(section.start, section.end, source);
});
if (processedKeywords.size === 0) {
alert('No keywords found in either section. Please make sure you\'re on the correct page.');
return;
}
// Create a temporary textarea to handle large amounts of text
const textarea = document.createElement('textarea');
textarea.value = csvContent;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
const headerStatus = window.includeHeaders ? 'with headers ' : '';
notification.textContent = `${processedKeywords.size} unique keywords copied to clipboard ${headerStatus}in CSV format!`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
} catch (err) {
alert('Failed to copy keywords: ' + err.message);
} finally {
document.body.removeChild(textarea);
}
}
// 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;
function tryFindContent() {
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 keyword extraction...');
// Wait a bit more for all content to load
setTimeout(extractKeywords, 500);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(tryFindContent, 1000); // Wait 1 second before trying again
} else {
alert('Could not find keyword content after multiple attempts. Please make sure you are on the correct page.');
}
}
tryFindContent();
}
waitForContent();
})();

BIN
icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

BIN
icon96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

18
manifest.json Normal file
View file

@ -0,0 +1,18 @@
{
"manifest_version": 2,
"name": "Keyword Scraper",
"version": "1.0",
"description": "Scrapes keywords from Google Keyword Planner",
"permissions": [
"activeTab",
"clipboardWrite",
"*://*.google.com/*"
],
"browser_action": {
"default_popup": "popup.html"
},
"icons": {
"48": "icon48.png",
"96": "icon96.png"
}
}

91
popup.html Normal file
View file

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
width: 200px;
margin: 0;
padding: 10px;
font-family: Arial, sans-serif;
transition: all 0.3s ease;
}
body.dialog-open {
width: 300px;
height: 200px;
}
#grabKeywords {
width: 100%;
padding: 8px;
margin: 5px 0;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
}
#grabKeywords:hover {
background-color: #45a049;
}
#dialog {
display: none;
position: absolute;
left: 10px;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
}
#dialog p {
margin: 0 0 15px 0;
font-size: 14px;
line-height: 1.4;
}
.dialog-buttons {
display: flex;
justify-content: space-between;
gap: 10px;
}
.dialog-buttons button {
flex: 1;
padding: 8px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
}
.dialog-buttons button:hover {
background-color: #45a049;
}
#overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
}
</style>
</head>
<body>
<button id="grabKeywords">Grab GKWP Keywords</button>
<div id="overlay"></div>
<div id="dialog">
<p>Would you like to include column headers in the export?</p>
<div class="dialog-buttons">
<button id="yesButton">Yes</button>
<button id="noButton">No</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

41
popup.js Normal file
View file

@ -0,0 +1,41 @@
document.getElementById('grabKeywords').addEventListener('click', function() {
// Add class to body to expand it
document.body.classList.add('dialog-open');
// Show the dialog and overlay
document.getElementById('dialog').style.display = 'block';
document.getElementById('overlay').style.display = 'block';
});
async function handleDialogChoice(includeHeaders) {
try {
// Hide the dialog and overlay
document.getElementById('dialog').style.display = 'none';
document.getElementById('overlay').style.display = 'none';
// Remove the expanded class
document.body.classList.remove('dialog-open');
// Get the active tab
const tabs = await browser.tabs.query({active: true, currentWindow: true});
const activeTab = tabs[0];
// First inject the content script
await browser.tabs.executeScript(activeTab.id, {
code: `window.includeHeaders = ${includeHeaders};`
});
// Then execute the main content script
await browser.tabs.executeScript(activeTab.id, {
file: 'content-script.js'
});
// Close the popup after initiating the content script
window.close();
} catch (error) {
console.error('Error:', error);
alert('Error: ' + error.message);
}
}
// Add click handlers for the Yes and No buttons
document.getElementById('yesButton').addEventListener('click', () => handleDialogChoice(true));
document.getElementById('noButton').addEventListener('click', () => handleDialogChoice(false));