supapup
Version:
⚡ Lightning-fast MCP browser dev tool. Navigate → Get instant structured data. No screenshots needed! Puppeteer: 📸 → CSS selectors → JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.
231 lines (202 loc) • 8.47 kB
JavaScript
/**
* Client-side hover detection script for Supapup
* This gets injected into pages to detect hoverable elements
*/
export const HOVER_DETECTION_SCRIPT = `
(function() {
// Check if already initialized
if (window.__SUPAPUP_HOVER_DETECTOR__) return;
window.__SUPAPUP_HOVER_DETECTOR__ = {
detectHoverableElements: function() {
const hoverable = [];
let hoverIndex = 0;
const processedElements = new Set();
// Helper to generate unique selector
function getSelector(element) {
if (element.id) return '#' + element.id;
if (element.className && typeof element.className === 'string') {
const classes = element.className.trim().split(/\\s+/);
if (classes.length && classes[0]) return '.' + classes[0];
}
// Generate path-based selector
let path = [];
let current = element;
while (current && current.nodeType === Node.ELEMENT_NODE) {
let selector = current.nodeName.toLowerCase();
if (current.id) {
selector = '#' + current.id;
path.unshift(selector);
break;
} else if (current.className && typeof current.className === 'string') {
selector += '.' + current.className.trim().split(/\\s+/)[0];
}
path.unshift(selector);
current = current.parentNode;
}
return path.join(' > ');
}
// Check if element or its children have hover styles
function checkHoverability(element) {
const computed = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
// Skip invisible elements
if (rect.width === 0 || rect.height === 0 ||
computed.display === 'none' ||
computed.visibility === 'hidden' ||
computed.opacity === '0') {
return null;
}
let confidence = 0;
let hoverType = 'none';
const indicators = [];
// 1. Check transitions/animations
if (computed.transition !== 'none' && computed.transition !== 'all 0s ease 0s') {
confidence += 0.3;
hoverType = 'css';
indicators.push('has-transition');
}
// 2. Check cursor
if (computed.cursor === 'pointer') {
confidence += 0.2;
indicators.push('pointer-cursor');
}
// 3. Check interactive elements
const tagName = element.tagName.toLowerCase();
const interactiveTags = ['a', 'button', 'input', 'select', 'textarea'];
if (interactiveTags.includes(tagName)) {
confidence += 0.3;
hoverType = 'css';
indicators.push('interactive-tag');
}
// 4. Check class names
const className = element.className?.toString() || '';
const hoverPatterns = ['hover', 'dropdown', 'tooltip', 'menu', 'btn', 'link'];
const hasHoverClass = hoverPatterns.some(pattern =>
className.toLowerCase().includes(pattern)
);
if (hasHoverClass) {
confidence += 0.2;
indicators.push('hover-class');
}
// 5. Check for hover event listeners
const hasMouseEvents = !!(
element.onmouseover ||
element.onmouseenter ||
element.onmouseleave ||
element.onmouseout
);
if (hasMouseEvents) {
confidence += 0.3;
hoverType = hoverType === 'css' ? 'both' : 'javascript';
indicators.push('mouse-events');
}
// 6. Check for dropdown pattern (hidden children)
let dropdownContent = null;
const children = element.querySelectorAll('*');
for (let child of children) {
const childStyle = window.getComputedStyle(child);
if (childStyle.display === 'none' || childStyle.visibility === 'hidden') {
// Check if this might be dropdown content
const childClasses = child.className?.toString() || '';
if (childClasses.includes('dropdown') ||
childClasses.includes('menu') ||
childClasses.includes('content') ||
childClasses.includes('tooltip')) {
dropdownContent = child;
confidence += 0.3;
hoverType = 'css';
indicators.push('has-dropdown');
break;
}
}
}
// 7. Check if element is inside a hoverable parent
let parentHoverable = null;
let parent = element.parentElement;
while (parent && parent !== document.body) {
const parentClass = parent.className?.toString() || '';
if (parentClass.includes('dropdown') || parentClass.includes('menu')) {
parentHoverable = parent;
break;
}
parent = parent.parentElement;
}
if (confidence > 0.3) {
return {
confidence: Math.min(confidence, 1),
hoverType,
indicators,
dropdownContent,
parentHoverable
};
}
return null;
}
// Process all elements
const elements = document.querySelectorAll('*');
elements.forEach(element => {
// Skip if already processed
if (processedElements.has(element)) return;
processedElements.add(element);
const hoverData = checkHoverability(element);
if (!hoverData) return;
const hoverId = 'hover-' + hoverIndex++;
const selector = getSelector(element);
// Mark element with data attributes
element.setAttribute('data-mcp-hoverable', 'true');
element.setAttribute('data-mcp-hover-id', hoverId);
element.setAttribute('data-mcp-hover-confidence', hoverData.confidence.toString());
const elementData = {
id: hoverId,
selector: selector,
tagName: element.tagName.toLowerCase(),
text: element.textContent?.trim().substring(0, 50),
type: element.type || element.getAttribute('type') || '',
hasTransition: hoverData.indicators.includes('has-transition'),
transitionDuration: window.getComputedStyle(element).transitionDuration,
hoverType: hoverData.hoverType,
confidence: hoverData.confidence,
indicators: hoverData.indicators
};
// Handle dropdown patterns
if (hoverData.dropdownContent) {
elementData.isDropdownTrigger = true;
const dropdownId = 'hover-dropdown-' + hoverIndex++;
hoverData.dropdownContent.setAttribute('data-mcp-hover-dropdown-id', dropdownId);
elementData.dropdownContentId = dropdownId;
}
// Handle parent hover patterns
if (hoverData.parentHoverable && !hoverData.parentHoverable.hasAttribute('data-mcp-hover-id')) {
const parentId = 'hover-parent-' + hoverIndex++;
hoverData.parentHoverable.setAttribute('data-mcp-hoverable', 'true');
hoverData.parentHoverable.setAttribute('data-mcp-hover-id', parentId);
elementData.parentHoverId = parentId;
// Also add the parent as a hoverable element
hoverable.push({
id: parentId,
selector: getSelector(hoverData.parentHoverable),
tagName: hoverData.parentHoverable.tagName.toLowerCase(),
text: 'Container for: ' + (elementData.text || '').substring(0, 30),
type: 'container',
hasTransition: false,
transitionDuration: '0s',
hoverType: 'css',
confidence: 0.8,
indicators: ['dropdown-container']
});
}
hoverable.push(elementData);
});
// Sort by confidence and filter low-confidence items
return hoverable
.filter(el => el.confidence > 0.4)
.sort((a, b) => b.confidence - a.confidence);
}
};
// Auto-detect on script injection
const hoverableElements = window.__SUPAPUP_HOVER_DETECTOR__.detectHoverableElements();
console.log('[Supapup] Detected ' + hoverableElements.length + ' hoverable elements');
// Store for later use
window.__SUPAPUP_HOVERABLE_ELEMENTS__ = hoverableElements;
})();
`;