Skip to content

Instantly share code, notes, and snippets.

@MarwanShehata
Last active May 17, 2025 18:37
Show Gist options
  • Save MarwanShehata/01ffccceff91640563cb32bfbedf26b6 to your computer and use it in GitHub Desktop.
Save MarwanShehata/01ffccceff91640563cb32bfbedf26b6 to your computer and use it in GitHub Desktop.
DOM Parent Collector
// ==UserScript==
// @name DOM Parent Collector
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Collect DOM parents of clicked elements and organize them
// @author You
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM_download
// ==/UserScript==
(function() {
'use strict';
// Configuration
let config = GM_getValue('domParentCollectorConfig', {
parentLevel: 5, // Default parent level to grab
enableCollection: true, // Collection enabled by default
maxStoredItems: 100, // Maximum number of items to store
showClickHighlight: true, // Show highlight effect when clicking
autoGroupInputs: true, // Auto-group inputs with labels
defaultExportFormat: 'json' // Default export format
});
// Save config helper
function saveConfig() {
GM_setValue('domParentCollectorConfig', config);
}
// Initialize storage if not exists
if (!GM_getValue('collectedElements')) {
GM_setValue('collectedElements', []);
}
// Styles for UI elements
GM_addStyle(`
.dom-collector-ui {
position: fixed;
bottom: 20px;
right: 20px;
background: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
z-index: 9999;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
max-width: 300px;
font-family: Arial, sans-serif;
font-size: 12px;
}
.dom-collector-ui button {
margin: 5px;
padding: 5px 10px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
.dom-collector-ui button:hover {
background: #e0e0e0;
}
.dom-collector-ui h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 14px;
}
.dom-collector-counter {
display: inline-block;
background: #007bff;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
margin-left: 5px;
}
.dom-collector-highlight {
outline: 2px dashed red !important;
background-color: rgba(255, 0, 0, 0.1) !important;
transition: all 0.3s ease;
}
.dom-collector-settings {
padding: 10px;
background: #f9f9f9;
border-top: 1px solid #eee;
margin-top: 10px;
}
.dom-collector-settings label {
display: block;
margin: 5px 0;
}
.dom-collector-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #eee;
padding: 5px;
margin-top: 10px;
}
.dom-collector-list-item {
padding: 3px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
}
.dom-collector-list-item:hover {
background: #f5f5f5;
}
.dom-collector-export {
padding: 10px;
background: #f9f9f9;
border-top: 1px solid #eee;
margin-top: 10px;
display: none;
}
.dom-collector-export select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
`);
// Create UI
function createUI() {
const uiContainer = document.createElement('div');
uiContainer.className = 'dom-collector-ui';
uiContainer.innerHTML = `
<h3>DOM Parent Collector <span class="dom-collector-counter">0</span></h3>
<button id="toggle-collection">Pause Collection</button>
<button id="toggle-settings">Settings</button>
<button id="view-collected">View Collected</button>
<button id="export-collected">Export</button>
<button id="clear-collected">Clear All</button>
<div id="dom-collector-settings" class="dom-collector-settings" style="display: none;">
<label>
Parent Level:
<input type="number" id="parent-level" min="1" max="20" value="${config.parentLevel}">
</label>
<label>
<input type="checkbox" id="show-highlight" ${config.showClickHighlight ? 'checked' : ''}>
Show click highlights
</label>
<label>
<input type="checkbox" id="auto-group" ${config.autoGroupInputs ? 'checked' : ''}>
Auto-group inputs with labels
</label>
<label>
Max stored items:
<input type="number" id="max-stored" min="10" max="1000" value="${config.maxStoredItems}">
</label>
<label>
Default export format:
<select id="default-export-format">
<option value="json" ${config.defaultExportFormat === 'json' ? 'selected' : ''}>JSON</option>
<option value="csv" ${config.defaultExportFormat === 'csv' ? 'selected' : ''}>CSV</option>
<option value="html" ${config.defaultExportFormat === 'html' ? 'selected' : ''}>HTML</option>
<option value="plaintext" ${config.defaultExportFormat === 'plaintext' ? 'selected' : ''}>Plain Text</option>
</select>
</label>
<button id="save-settings">Save Settings</button>
</div>
<div id="dom-collector-list" class="dom-collector-list" style="display: none;"></div>
<div id="dom-collector-export" class="dom-collector-export" style="display: none;">
<label>
Export Format:
<select id="export-format">
<option value="json">JSON</option>
<option value="csv">CSV</option>
<option value="html">HTML</option>
<option value="plaintext">Plain Text</option>
</select>
</label>
<button id="copy-to-clipboard">Copy to Clipboard</button>
<button id="download-file">Download File</button>
<button id="cancel-export">Cancel</button>
</div>
`;
document.body.appendChild(uiContainer);
// Update counter
updateCollectionCounter();
// Event listeners
document.getElementById('toggle-collection').addEventListener('click', toggleCollection);
document.getElementById('toggle-settings').addEventListener('click', toggleSettings);
document.getElementById('view-collected').addEventListener('click', toggleViewCollected);
document.getElementById('export-collected').addEventListener('click', toggleExportPanel);
document.getElementById('clear-collected').addEventListener('click', clearCollected);
document.getElementById('save-settings').addEventListener('click', saveSettings);
document.getElementById('copy-to-clipboard').addEventListener('click', copyToClipboard);
document.getElementById('download-file').addEventListener('click', downloadFile);
document.getElementById('cancel-export').addEventListener('click', cancelExport);
}
// Toggle collection on/off
function toggleCollection() {
config.enableCollection = !config.enableCollection;
document.getElementById('toggle-collection').textContent =
config.enableCollection ? 'Pause Collection' : 'Resume Collection';
saveConfig();
}
// Toggle settings panel
function toggleSettings() {
const settingsDiv = document.getElementById('dom-collector-settings');
const listDiv = document.getElementById('dom-collector-list');
const exportDiv = document.getElementById('dom-collector-export');
if (settingsDiv.style.display === 'none') {
settingsDiv.style.display = 'block';
listDiv.style.display = 'none';
exportDiv.style.display = 'none';
} else {
settingsDiv.style.display = 'none';
}
}
// Save settings
function saveSettings() {
config.parentLevel = parseInt(document.getElementById('parent-level').value, 10) || 5;
config.showClickHighlight = document.getElementById('show-highlight').checked;
config.autoGroupInputs = document.getElementById('auto-group').checked;
config.maxStoredItems = parseInt(document.getElementById('max-stored').value, 10) || 100;
config.defaultExportFormat = document.getElementById('default-export-format').value;
saveConfig();
document.getElementById('dom-collector-settings').style="display: none;";
}
// Toggle view collected items
function toggleViewCollected() {
const listDiv = document.getElementById('dom-collector-list');
const settingsDiv = document.getElementById('dom-collector-settings');
const exportDiv = document.getElementById('dom-collector-export');
if (listDiv.style.display === 'none') {
listDiv.style.display = 'block';
settingsDiv.style.display = 'none';
exportDiv.style.display = 'none';
// Populate the list
const collectedElements = GM_getValue('collectedElements', []);
listDiv.innerHTML = '';
if (collectedElements.length === 0) {
listDiv.innerHTML = '<p>No elements collected yet.</p>';
return;
}
// Group by tag and sort
const groupedElements = {};
collectedElements.forEach(item => {
if (!groupedElements[item.parentTag]) {
groupedElements[item.parentTag] = [];
}
groupedElements[item.parentTag].push(item);
});
// Create list items
Object.keys(groupedElements).sort().forEach(tag => {
const items = groupedElements[tag];
const groupHeader = document.createElement('div');
groupHeader.innerHTML = `<strong>${tag} (${items.length})</strong>`;
listDiv.appendChild(groupHeader);
items.forEach((item, index) => {
const listItem = document.createElement('div');
listItem.className = 'dom-collector-list-item';
listItem.textContent = `${index + 1}. ${item.description.substring(0, 50)}${item.description.length > 50 ? '...' : ''}`;
listItem.title = item.fullPath;
// Add click handler to show full details
listItem.addEventListener('click', () => {
alert(`Element: ${item.description}\nFull Path: ${item.fullPath}\nClasses: ${item.classes}\nID: ${item.id}`);
});
listDiv.appendChild(listItem);
});
});
} else {
listDiv.style.display = 'none';
}
}
// Toggle export panel
function toggleExportPanel() {
const listDiv = document.getElementById('dom-collector-list');
const settingsDiv = document.getElementById('dom-collector-settings');
const exportDiv = document.getElementById('dom-collector-export');
if (exportDiv.style.display === 'none') {
exportDiv.style.display = 'block';
settingsDiv.style.display = 'none';
listDiv.style.display = 'none';
// Set the default export format
document.getElementById('export-format').value = config.defaultExportFormat;
} else {
exportDiv.style.display = 'none';
}
}
// Copy collected elements to clipboard
function copyToClipboard() {
const format = document.getElementById('export-format').value;
const collectedElements = GM_getValue('collectedElements', []);
if (collectedElements.length === 0) {
alert('No elements collected yet.');
return;
}
const exportData = formatExportData(collectedElements, format);
GM_setClipboard(exportData);
alert('Data copied to clipboard!');
}
// Download collected elements as a file
function downloadFile() {
const format = document.getElementById('export-format').value;
const collectedElements = GM_getValue('collectedElements', []);
if (collectedElements.length === 0) {
alert('No elements collected yet.');
return;
}
const exportData = formatExportData(collectedElements, format);
const blob = new Blob([exportData], { type: getMimeType(format) });
const url = URL.createObjectURL(blob);
const filename = `dom-parents-${new Date().toISOString().split('T')[0]}.${getFileExtension(format)}`;
// Use GM_download if available, otherwise fallback to creating a link
if (typeof GM_download !== 'undefined') {
GM_download({
url: url,
name: filename,
saveAs: true
});
} else {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 100);
}
}
// Cancel export
function cancelExport() {
document.getElementById('dom-collector-export').style.display = 'none';
}
// Format export data based on selected format
function formatExportData(data, format) {
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'csv':
// Determine all possible keys from all objects
const allKeys = new Set();
data.forEach(item => {
Object.keys(item).forEach(key => allKeys.add(key));
});
const headers = Array.from(allKeys);
let csv = headers.join(',') + '\n';
data.forEach(item => {
const row = headers.map(header => {
const value = item[header] || '';
// Escape commas and quotes
return `"${String(value).replace(/"/g, '""')}"`;
});
csv += row.join(',') + '\n';
});
return csv;
case 'html':
// Create HTML table
let html = '<table border="1" cellpadding="5" cellspacing="0">\n<thead>\n<tr>\n';
// Group by parent tag
const groupedElements = {};
data.forEach(item => {
if (!groupedElements[item.parentTag]) {
groupedElements[item.parentTag] = [];
}
groupedElements[item.parentTag].push(item);
});
// Headers
html += '<th>Element</th><th>Description</th><th>Parent Tag</th><th>Parent ID</th><th>Classes</th><th>Path</th>\n';
html += '</tr>\n</thead>\n<tbody>\n';
// Table rows
Object.keys(groupedElements).sort().forEach(tag => {
const items = groupedElements[tag];
items.forEach(item => {
html += '<tr>\n';
html += `<td>${escapeHtml(item.elementTag)}</td>\n`;
html += `<td>${escapeHtml(item.description)}</td>\n`;
html += `<td>${escapeHtml(item.parentTag)}</td>\n`;
html += `<td>${escapeHtml(item.parentId)}</td>\n`;
html += `<td>${escapeHtml(item.classes)}</td>\n`;
html += `<td>${escapeHtml(item.fullPath)}</td>\n`;
html += '</tr>\n';
});
});
html += '</tbody>\n</table>';
return html;
case 'plaintext':
// Simple text format
let text = 'DOM PARENT COLLECTOR EXPORT\n';
text += '===========================\n\n';
// Group by parent tag
const groupedElementsText = {};
data.forEach(item => {
if (!groupedElementsText[item.parentTag]) {
groupedElementsText[item.parentTag] = [];
}
groupedElementsText[item.parentTag].push(item);
});
Object.keys(groupedElementsText).sort().forEach(tag => {
text += `## ${tag.toUpperCase()} (${groupedElementsText[tag].length} items)\n\n`;
groupedElementsText[tag].forEach((item, index) => {
text += `${index + 1}. ${item.description}\n`;
text += ` - Element: ${item.elementTag}\n`;
text += ` - Parent: ${item.parentTag}\n`;
text += ` - ID: ${item.parentId}\n`;
text += ` - Classes: ${item.classes}\n`;
text += ` - Path: ${item.fullPath}\n\n`;
});
});
return text;
default:
return JSON.stringify(data, null, 2);
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// Get file extension based on format
function getFileExtension(format) {
switch (format) {
case 'json': return 'json';
case 'csv': return 'csv';
case 'html': return 'html';
case 'plaintext': return 'txt';
default: return 'txt';
}
}
// Get MIME type based on format
function getMimeType(format) {
switch (format) {
case 'json': return 'application/json';
case 'csv': return 'text/csv';
case 'html': return 'text/html';
case 'plaintext': return 'text/plain';
default: return 'text/plain';
}
}
// Clear collected elements
function clearCollected() {
if (confirm('Are you sure you want to clear all collected elements?')) {
GM_setValue('collectedElements', []);
updateCollectionCounter();
if (document.getElementById('dom-collector-list').style.display !== 'none') {
toggleViewCollected(); // Refresh the list view
}
}
}
// Update collection counter
function updateCollectionCounter() {
const collectedElements = GM_getValue('collectedElements', []);
document.querySelector('.dom-collector-counter').textContent = collectedElements.length;
}
// Find the nth parent of an element
function getNthParent(element, n) {
let parent = element;
for (let i = 0; i < n; i++) {
if (parent.parentElement) {
parent = parent.parentElement;
} else {
break;
}
}
return parent;
}
// Get a simplified path to the element
function getElementPath(element) {
let path = [];
let current = element;
while (current && current !== document.documentElement) {
let selector = current.tagName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
} else if (current.className && typeof current.className === 'string') {
selector += `.${current.className.trim().split(/\s+/).join('.')}`;
}
path.unshift(selector);
current = current.parentElement;
}
return path.join(' > ');
}
// Find associated label for an input element
function findInputLabel(inputElement) {
// First check for label with 'for' attribute
if (inputElement.id) {
const label = document.querySelector(`label[for="${inputElement.id}"]`);
if (label) return label;
}
// Then check for parent label
let parent = inputElement.parentElement;
while (parent && parent !== document.body) {
if (parent.tagName === 'LABEL') {
return parent;
}
parent = parent.parentElement;
}
// Finally check for adjacent label (common pattern)
const previousSibling = inputElement.previousElementSibling;
const nextSibling = inputElement.nextElementSibling;
if (previousSibling && previousSibling.tagName === 'LABEL') {
return previousSibling;
}
if (nextSibling && nextSibling.tagName === 'LABEL') {
return nextSibling;
}
return null;
}
// Get a description of the element
function getElementDescription(element) {
// For text inputs, return the value or placeholder
if (element.tagName === 'INPUT' && element.type === 'text') {
return element.value || element.placeholder || 'Text input';
}
// For checkboxes, return the checked state
if (element.tagName === 'INPUT' && element.type === 'checkbox') {
return `Checkbox: ${element.checked ? 'checked' : 'unchecked'}`;
}
// For buttons, return the text content or value
if (element.tagName === 'BUTTON' || (element.tagName === 'INPUT' &&
(element.type === 'button' || element.type === 'submit'))) {
return element.textContent || element.value || 'Button';
}
// For select elements, return the selected option text
if (element.tagName === 'SELECT' && element.options[element.selectedIndex]) {
return `Select: ${element.options[element.selectedIndex].text}`;
}
// For links, return the text content or href
if (element.tagName === 'A') {
return element.textContent.trim() || element.href || 'Link';
}
// For images, return the alt text or src
if (element.tagName === 'IMG') {
return element.alt || element.src.split('/').pop() || 'Image';
}
// For other elements, return the text content or tag name
return element.textContent.trim().substring(0, 100) ||
`${element.tagName.toLowerCase()}${element.id ? '#' + element.id : ''}`;
}
// Process clicked element
function processClickedElement(event) {
if (!config.enableCollection) return;
const element = event.target;
// Highlight the clicked element if enabled
if (config.showClickHighlight) {
element.classList.add('dom-collector-highlight');
setTimeout(() => {
element.classList.remove('dom-collector-highlight');
}, 1000);
}
// Get the nth parent
const parent = getNthParent(element, config.parentLevel);
// Base element data
let elementData = {
timestamp: Date.now(),
elementTag: element.tagName.toLowerCase(),
elementType: element.type || 'none',
parentTag: parent.tagName.toLowerCase(),
parentId: parent.id || 'none',
parentClasses: parent.className || 'none',
description: getElementDescription(element),
fullPath: getElementPath(parent),
id: parent.id || 'none',
classes: Array.from(parent.classList).join(' ') || 'none'
};
// Special handling for inputs
if (config.autoGroupInputs &&
(element.tagName === 'INPUT' || element.tagName === 'SELECT' ||
element.tagName === 'TEXTAREA' || element.tagName === 'BUTTON')) {
const label = findInputLabel(element);
if (label) {
elementData.label = label.textContent.trim();
elementData.description = `${elementData.label}: ${elementData.description}`;
// Also get the nth parent of the label
const labelParent = getNthParent(label, config.parentLevel);
elementData.labelParentTag = labelParent.tagName.toLowerCase();
elementData.labelParentPath = getElementPath(labelParent);
}
}
// Store the collected element
saveCollectedElement(elementData);
// Don't block the normal behavior of the page
return true;
}
// Save collected element to storage
function saveCollectedElement(elementData) {
let collectedElements = GM_getValue('collectedElements', []);
// Add the new element
collectedElements.push(elementData);
// Sort by parent tag
collectedElements.sort((a, b) => a.parentTag.localeCompare(b.parentTag));
// Limit to max stored items
if (collectedElements.length > config.maxStoredItems) {
collectedElements = collectedElements.slice(-config.maxStoredItems);
}
// Save and update counter
GM_setValue('collectedElements', collectedElements);
updateCollectionCounter();
}
// Add Tampermonkey menu command
GM_registerMenuCommand('DOM Parent Collector Settings', () => {
document.getElementById('toggle-settings').click();
});
// Function to handle input blur events
function handleInputBlur(event) {
if (!config.enableCollection) return;
const element = event.target;
// Get the nth parent
const parent = getNthParent(element, config.parentLevel);
// Base element data
let elementData = {
timestamp: Date.now(),
elementTag: element.tagName.toLowerCase(),
elementType: element.type || 'none',
parentTag: parent.tagName.toLowerCase(),
parentId: parent.id || 'none',
parentClasses: parent.className || 'none',
description: `Value: ${element.value}`,
fullPath: getElementPath(parent),
id: parent.id || 'none',
classes: Array.from(parent.classList).join(' ') || 'none'
};
// Special handling for inputs
if (config.autoGroupInputs) {
const label = findInputLabel(element);
if (label) {
elementData.label = label.textContent.trim();
elementData.description = `${elementData.label}: ${elementData.description}`;
// Also get the nth parent of the label
const labelParent = getNthParent(label, config.parentLevel);
elementData.labelParentTag = labelParent.tagName.toLowerCase();
elementData.labelParentPath = getElementPath(labelParent);
}
}
// Store the collected element
saveCollectedElement(elementData);
}
// Function to handle select change events
function handleSelectChange(event) {
if (!config.enableCollection) return;
const element = event.target;
// Get all options and their values
const options = Array.from(element.options).map(option => ({
text: option.text,
value: option.value,
selected: option.selected
}));
// Create a Set of all values
const valuesSet = new Set(options.map(option => option.value));
// Get the nth parent
const parent = getNthParent(element, config.parentLevel);
// Base element data
let elementData = {
timestamp: Date.now(),
elementTag: element.tagName.toLowerCase(),
elementType: element.type || 'none',
parentTag: parent.tagName.toLowerCase(),
parentId: parent.id || 'none',
parentClasses: parent.className || 'none',
description: `Selected: ${element.options[element.selectedIndex].text}`,
fullPath: getElementPath(parent),
id: parent.id || 'none',
classes: Array.from(parent.classList).join(' ') || 'none',
options: options,
values: Array.from(valuesSet),
selectedValue: element.value
};
// Special handling for inputs
if (config.autoGroupInputs) {
const label = findInputLabel(element);
if (label) {
elementData.label = label.textContent.trim();
elementData.description = `${elementData.label}: ${elementData.description}`;
// Also get the nth parent of the label
const labelParent = getNthParent(label, config.parentLevel);
elementData.labelParentTag = labelParent.tagName.toLowerCase();
elementData.labelParentPath = getElementPath(labelParent);
}
}
// Store the collected element
saveCollectedElement(elementData);
}
// Start the script
window.addEventListener('load', () => {
createUI();
document.addEventListener('click', processClickedElement, true);
// Add event listeners for input blur and select change
document.querySelectorAll('input, textarea').forEach(input => {
input.addEventListener('blur', handleInputBlur);
});
document.querySelectorAll('select').forEach(select => {
select.addEventListener('change', handleSelectChange);
});
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment