react-tailwind-element-inspector
Version:
Inspect DOM elements hierarchy and classNames on hover in React + Tailwind projects.
414 lines (413 loc) • 18.1 kB
JavaScript
;
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();