UNPKG

react-tailwind-element-inspector

Version:

Inspect DOM elements hierarchy and classNames on hover in React + Tailwind projects.

414 lines (413 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.enableInspector = enableInspector; exports.disableInspector = disableInspector; let isActive = false; const overlays = []; // Panel and settings related global variables let inspectorPanel = null; let panelHeader = null; // For dragging let panelContentTree = null; // For tree view let lastInspectedElement = null; let activeInspectorToggle = null; const LOCAL_STORAGE_KEY = 'elementInspectorSettings'; // Default settings const defaultInspectorSettings = { showTag: true, showClasses: true, showId: true, showParents: true, showComputedStyles: true, showSiblingOverlays: true, showParentOverlays: true, maxParentDepth: 5, showAttributes: false, // New setting }; let inspectorSettings = Object.assign({}, defaultInspectorSettings); // --- Local Storage Functions --- function loadSettings() { const storedSettings = localStorage.getItem(LOCAL_STORAGE_KEY); if (storedSettings) { try { const parsed = JSON.parse(storedSettings); // Merge with defaults to ensure all keys are present if new settings are added inspectorSettings = Object.assign(Object.assign({}, defaultInspectorSettings), parsed); } catch (e) { console.error("Error parsing inspector settings from localStorage", e); inspectorSettings = Object.assign({}, defaultInspectorSettings); } } else { inspectorSettings = Object.assign({}, defaultInspectorSettings); } } function saveSettings() { try { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(inspectorSettings)); } catch (e) { console.error("Error saving inspector settings to localStorage", e); } } // --- Utility Functions (getParents, getSiblings) --- function getParents(el) { const parents = []; let current = el.parentElement; let depth = 0; while (current && current !== document.body && depth < inspectorSettings.maxParentDepth) { parents.push(current); current = current.parentElement; depth++; } return parents; } function getSiblings(el) { const parent = el.parentElement; if (!parent) return []; return Array.from(parent.children).filter(c => c !== el); } // --- Overlay Creation --- function createOverlay(rect, className, level) { const overlay = document.createElement('div'); overlay.classList.add('inspector-overlay', className); const pad = 2; // Reduced padding for a tighter fit const top = rect.top + window.scrollY - pad; const left = rect.left + window.scrollX - pad; const width = rect.width + pad * 2; const height = rect.height + pad * 2; Object.assign(overlay.style, { position: 'absolute', top: top + 'px', left: left + 'px', width: width + 'px', height: height + 'px', pointerEvents: 'none', zIndex: (9998 - (level || 0)).toString(), // Adjust z-index based on level for parents }); const levelMatch = className.match(/inspector-parent-(\d+)-overlay/); if (levelMatch || level !== undefined) { overlay.dataset.level = (level !== undefined ? level : levelMatch[1]).toString(); } document.body.appendChild(overlay); overlays.push(overlay); } // --- Inspector Panel Content Update (Tree View) --- function updateInspectorPanelContent(targetElement) { if (!inspectorPanel || !panelContentTree) return; panelContentTree.innerHTML = ''; // Clear previous tree const buildTree = (el, depth, являетсяЦелью) => { const fragment = document.createDocumentFragment(); const item = document.createElement('div'); item.className = 'inspector-tree-item'; item.style.marginLeft = `${depth * 15}px`; // Indentation item.dataset.elementType = являетсяЦелью ? 'target' : 'parent'; let itemContent = ''; if (inspectorSettings.showTag) { itemContent += `<span class="tag-name">${el.tagName.toLowerCase()}</span>`; } if (inspectorSettings.showId && el.id) { itemContent += `<span class="element-id">#${el.id}</span>`; } if (inspectorSettings.showClasses && el.className) { const classes = typeof el.className === 'string' ? el.className.trim() : ''; if (classes) { itemContent += ` <span class="element-classes">.${classes.split(' ').join('.')}</span>`; } } item.innerHTML = itemContent || '<em>(no identifiable info)</em>'; // Store reference to the actual element for selection item.__inspectorElement = el; item.addEventListener('click', (e) => { e.stopPropagation(); lastInspectedElement = el; // Update last inspected highlightElement(el); // Re-highlight this element // Optionally, re-render panel content if details depend on "target" status updateInspectorPanelContent(el); }); item.addEventListener('mouseover', (e) => { e.stopPropagation(); // Preview highlight (could be a different style or just reuse highlightElement) highlightElement(el, true); // Pass a flag for preview highlight if needed }); item.addEventListener('mouseout', (e) => { e.stopPropagation(); // If we had a preview highlight, remove it and restore main highlight if (lastInspectedElement) { highlightElement(lastInspectedElement); } else { cleanupHighlights(); } }); fragment.appendChild(item); if (являетсяЦелью) { // Only show details for the main target element in the tree for now let detailsContent = '<div class="inspector-details" style="margin-left: ' + (depth * 15 + 10) + 'px;">'; if (inspectorSettings.showAttributes) { let attrs = '<strong>Attributes:</strong><br/>'; for (let i = 0; i < el.attributes.length; i++) { const attr = el.attributes[i]; attrs += ` ${attr.name}: ${attr.value}<br/>`; } detailsContent += attrs; } if (inspectorSettings.showComputedStyles) { const comp = window.getComputedStyle(el); const styles = [ `display: ${comp.display}`, `position: ${comp.position}`, `font-size: ${comp.fontSize}`, `color: ${comp.color}`, `background: ${comp.backgroundColor || 'transparent'}`, `padding: ${comp.padding}`, `margin: ${comp.margin}`, ].map(s => ` ${s}`).join('<br/>'); detailsContent += `<strong>Computed Styles:</strong><br/>${styles}`; } detailsContent += '</div>'; if (detailsContent.includes('<strong>')) { // Only add if there are details const detailNode = document.createElement('div'); detailNode.innerHTML = detailsContent; fragment.appendChild(detailNode); } } return fragment; }; // Build tree for the target element panelContentTree.appendChild(buildTree(targetElement, 0, true)); // Build tree for parents if enabled if (inspectorSettings.showParents) { const parentElements = getParents(targetElement); parentElements.forEach((p, index) => { panelContentTree.appendChild(buildTree(p, index + 1, false)); }); } } // --- Draggable Functionality --- function makeDraggable(element, handle) { let isDragging = false; let offsetX, offsetY; const onMouseDown = (e) => { if (e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement || e.target instanceof HTMLLabelElement || e.target.closest('.inspector-settings-panel')) { return; } isDragging = true; offsetX = e.clientX - element.offsetLeft; offsetY = e.clientY - element.offsetTop; handle.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e) => { if (!isDragging) return; element.style.left = `${e.clientX - offsetX}px`; element.style.top = `${e.clientY - offsetY}px`; }; const onMouseUp = () => { if (isDragging) { isDragging = false; handle.style.cursor = 'grab'; document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } }; handle.addEventListener('mousedown', onMouseDown); } // --- Settings Panel Logic --- function toggleSettingsPanel() { if (!inspectorPanel) return; const settingsPanelDiv = inspectorPanel.querySelector('.inspector-settings-panel'); if (settingsPanelDiv) { const isHidden = settingsPanelDiv.style.display === 'none'; settingsPanelDiv.style.display = isHidden ? 'block' : 'none'; } } function handleSettingChange(event) { const input = event.target; const settingName = input.dataset.setting; if (settingName && settingName in inspectorSettings) { if (input.type === 'checkbox') { inspectorSettings[settingName] = input.checked; } else if (input.type === 'number') { inspectorSettings[settingName] = parseInt(input.value, 10); } saveSettings(); // Save to localStorage if (lastInspectedElement && isActive) { updateInspectorPanelContent(lastInspectedElement); // Re-render panel highlightElement(lastInspectedElement); // Re-apply highlights based on new settings } } } // --- Inspector Panel Creation --- function ensureInspectorPanel() { if (inspectorPanel) return; loadSettings(); // Load settings when panel is first created inspectorPanel = document.createElement('div'); inspectorPanel.className = 'inspector-panel'; Object.assign(inspectorPanel.style, { position: 'fixed', top: '20px', left: '20px', zIndex: '10000', display: 'none', flexDirection: 'column', minWidth: '300px', minHeight: '200px', resize: 'both', overflow: 'hidden' // Changed from auto to hidden, content tree will scroll }); panelHeader = document.createElement('div'); panelHeader.className = 'inspector-panel-header'; const titleBar = document.createElement('div'); titleBar.className = 'title-bar'; // Enable/Disable Toggle activeInspectorToggle = document.createElement('input'); activeInspectorToggle.type = 'checkbox'; activeInspectorToggle.className = 'inspector-active-toggle'; activeInspectorToggle.title = 'Enable/Disable Inspector'; activeInspectorToggle.checked = isActive; activeInspectorToggle.addEventListener('change', () => { if (activeInspectorToggle.checked) { enableInspector(); } else { disableInspector(); } }); titleBar.appendChild(activeInspectorToggle); const headerTitle = document.createElement('span'); headerTitle.textContent = 'Element Inspector'; titleBar.appendChild(headerTitle); panelHeader.appendChild(titleBar); const settingsButton = document.createElement('button'); settingsButton.className = 'inspector-settings-button'; settingsButton.innerHTML = '⚙️'; settingsButton.title = 'Settings'; settingsButton.onclick = toggleSettingsPanel; panelHeader.appendChild(settingsButton); const settingsPanelDiv = document.createElement('div'); settingsPanelDiv.className = 'inspector-settings-panel'; settingsPanelDiv.style.display = 'none'; let settingsHTML = '<h4>Inspector Settings</h4>'; settingsHTML += '<label><input type="checkbox" data-setting="showTag" ' + (inspectorSettings.showTag ? 'checked' : '') + '> Show Tag Name</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showId" ' + (inspectorSettings.showId ? 'checked' : '') + '> Show Element ID</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showClasses" ' + (inspectorSettings.showClasses ? 'checked' : '') + '> Show Classes</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showParents" ' + (inspectorSettings.showParents ? 'checked' : '') + '> Show Parents in Tree</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showAttributes" ' + (inspectorSettings.showAttributes ? 'checked' : '') + '> Show Attributes</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showComputedStyles" ' + (inspectorSettings.showComputedStyles ? 'checked' : '') + '> Show Computed Styles</label>'; settingsHTML += '<hr/>'; settingsHTML += '<h5>Overlay Settings</h5>'; settingsHTML += '<label><input type="checkbox" data-setting="showParentOverlays" ' + (inspectorSettings.showParentOverlays ? 'checked' : '') + '> Show Parent Overlays</label>'; settingsHTML += '<label><input type="checkbox" data-setting="showSiblingOverlays" ' + (inspectorSettings.showSiblingOverlays ? 'checked' : '') + '> Show Sibling Overlays</label>'; settingsHTML += '<label>Max Parent Depth: <input type="number" data-setting="maxParentDepth" value="' + inspectorSettings.maxParentDepth + '" min="0" max="20" style="width: 50px;"></label>'; settingsPanelDiv.innerHTML = settingsHTML; panelContentTree = document.createElement('div'); panelContentTree.className = 'inspector-panel-content inspector-tree-view'; // Added new class panelContentTree.innerHTML = '<i>Hover over an element to inspect.</i>'; inspectorPanel.appendChild(panelHeader); inspectorPanel.appendChild(settingsPanelDiv); inspectorPanel.appendChild(panelContentTree); document.body.appendChild(inspectorPanel); if (panelHeader) makeDraggable(inspectorPanel, panelHeader); settingsPanelDiv.querySelectorAll('input[type="checkbox"], input[type="number"]').forEach(input => { input.addEventListener('change', handleSettingChange); input.addEventListener('input', handleSettingChange); // For number input }); } // --- Main Inspector Logic --- function enableInspector() { if (isActive) return; loadSettings(); // Load settings each time it's enabled isActive = true; ensureInspectorPanel(); if (inspectorPanel) inspectorPanel.style.display = 'flex'; if (activeInspectorToggle) activeInspectorToggle.checked = true; document.addEventListener("mouseover", handleMouseOver, true); // Use capture phase document.addEventListener("mouseout", handleMouseOut, true); // Use capture phase // Add click listener to select elements if panel is active document.addEventListener("click", handleDocumentClick, true); } function disableInspector() { if (!isActive) return; isActive = false; if (inspectorPanel) inspectorPanel.style.display = 'none'; if (activeInspectorToggle) activeInspectorToggle.checked = false; document.removeEventListener("mouseover", handleMouseOver, true); document.removeEventListener("mouseout", handleMouseOut, true); document.removeEventListener("click", handleDocumentClick, true); cleanupHighlights(); if (panelContentTree) panelContentTree.innerHTML = '<i>Inspector disabled.</i>'; lastInspectedElement = null; } function handleDocumentClick(e) { if (!isActive || !inspectorPanel || inspectorPanel.contains(e.target)) { // If inspector is not active, or click is inside the panel, do nothing. return; } // Prevent click from triggering actions on the page while inspector is active for selection e.preventDefault(); e.stopPropagation(); const target = e.target; lastInspectedElement = target; highlightElement(target); updateInspectorPanelContent(target); } function handleMouseOver(e) { if (!isActive) return; const target = e.target; if (!target || target === document.body || target === document.documentElement || (inspectorPanel === null || inspectorPanel === void 0 ? void 0 : inspectorPanel.contains(target))) { return; } // For live hovering, we don't set lastInspectedElement or update panel, only highlight highlightElement(target, true); // Preview highlight } function handleMouseOut(e) { if (!isActive) return; // If mouse moves to another element, mouseover on that will handle it. // If mouse moves out of window or to panel, restore highlight to lastInspectedElement. if (lastInspectedElement) { highlightElement(lastInspectedElement); } else { cleanupHighlights(); } } function highlightElement(el, isPreview = false) { cleanupHighlights(); // Overlay on target createOverlay(el.getBoundingClientRect(), isPreview ? 'inspector-preview-overlay' : 'inspector-hover-overlay'); if (inspectorSettings.showParentOverlays && !isPreview) { // Only show parent/sibling for non-preview getParents(el).forEach((p, idx) => { createOverlay(p.getBoundingClientRect(), `inspector-parent-${idx}-overlay`, idx); }); } if (inspectorSettings.showSiblingOverlays && !isPreview) { getSiblings(el).forEach(s => { createOverlay(s.getBoundingClientRect(), 'inspector-sibling-overlay'); }); } } function cleanupHighlights() { overlays.forEach(o => o.remove()); overlays.length = 0; } // Initialize settings on script load loadSettings();