UNPKG

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
/** * 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; })(); `;