Greasy Fork is available in English.
Intercept click events to display element style info in a draggable floating panel. One global panel supports iframes. Toggle with Ctrl+F1.
// ==UserScript== // @name Style Inspector // @namespace [email protected] // @version 1.7 // @description Intercept click events to display element style info in a draggable floating panel. One global panel supports iframes. Toggle with Ctrl+F1. // @author tp-wen // @license MIT // @match *://*.3d66.com/* // @grant GM_addStyle // @run-at document-start // ==/UserScript== (function() { 'use strict'; // State let isActive = false; let lastHighlighted = null; const isTop = window === window.top; // --- Message Communication --- // Protocol: // 1. STYLE_STATE: Broadcast active state (true/false) from Top to all frames (recursive). // 2. STYLE_DATA: Send style info from Frame to Top. // 3. STYLE_QUERY_STATE: Frame asks Top for current state (on load). window.addEventListener('message', (event) => { const data = event.data; if (!data || typeof data !== 'object') return; switch (data.type) { case 'STYLE_STATE': setActive(data.active); propagateState(data.active); // Pass it down to my children break; case 'STYLE_DATA': if (isTop) { updatePanelUI(data.payload); } break; case 'STYLE_TOGGLE': if (isTop) { const newState = !isActive; setActive(newState); propagateState(newState); } break; case 'STYLE_QUERY_STATE': // Received a query, reply if we know the state (Top knows best) // We reply to the source of the message if (isTop && isActive && event.source) { event.source.postMessage({ type: 'STYLE_STATE', active: isActive }, '*'); } break; case 'STYLE_HIGHLIGHT_ACTIVE': // An iframe is highlighting an element. // 1. Clear Top's own overlay (if any) hideOverlay(); // 2. Tell OTHER frames to clear their overlays if (isTop) { propagateClear(event.source); } break; case 'STYLE_CLEAR_OVERLAY': hideOverlay(); break; } }); function setActive(active) { if (isActive === active) return; isActive = active; // Update UI if I am Top if (isTop) { updateToggleStateUI(); // Ensure panel is visible when activating if (isActive && panel) { panel.style.display = 'block'; } } if (isActive) { document.body.style.cursor = 'crosshair'; } else { document.body.style.cursor = ''; hideOverlay(); } } function propagateState(active) { // Send to all direct subframes for (let i = 0; i < window.frames.length; i++) { try { window.frames[i].postMessage({ type: 'STYLE_STATE', active: active }, '*'); } catch(e) { // Ignore cross-origin access errors if any (postMessage is usually safe) } } } function propagateClear(excludeSource) { // Send CLEAR command to all frames except the one that requested it for (let i = 0; i < window.frames.length; i++) { const frame = window.frames[i]; if (frame === excludeSource) continue; try { frame.postMessage({ type: 'STYLE_CLEAR_OVERLAY' }, '*'); } catch(e) {} } } // --- Interaction Logic (Runs in all frames) --- // Shortcut Key document.addEventListener('keydown', (e) => { // Ctrl + F1 if (e.ctrlKey && e.key === 'F1') { e.preventDefault(); e.stopPropagation(); if (isTop) { const newState = !isActive; setActive(newState); propagateState(newState); } else { window.top.postMessage({ type: 'STYLE_TOGGLE' }, '*'); } } }, true); // Hover Highlight document.addEventListener('mouseover', (e) => { if (!isActive) return; // Don't highlight the panel itself (if it exists in this frame) if (isTop && document.getElementById('style-inspector-panel')?.contains(e.target)) return; // Don't highlight the overlay itself if (document.getElementById('inspector-overlay-container')?.contains(e.target)) return; updateOverlay(e.target); notifyHighlightActive(); }, true); document.addEventListener('mouseout', (e) => { if (!isActive) return; // Optional: clear overlay if mouse leaves window, but typically we just wait for next hover }, true); // Click Inspection document.addEventListener('click', (e) => { if (!isActive) return; // Don't block clicks on the panel itself if (isTop && document.getElementById('style-inspector-panel')?.contains(e.target)) return; if (document.getElementById('inspector-overlay-container')?.contains(e.target)) return; e.preventDefault(); e.stopPropagation(); const target = e.target; const styleData = getStyleData(target); if (isTop) { updatePanelUI(styleData); } else { // Send to top window.top.postMessage({ type: 'STYLE_DATA', payload: styleData }, '*'); } }, true); function getStyleData(el) { const computed = window.getComputedStyle(el); // Normalize tag let tagStr = el.tagName.toLowerCase(); if (el.id) tagStr += '#' + el.id; if (el.className && typeof el.className === 'string') { // Handle SVG className which is an object sometimes tagStr += '.' + el.className.split(' ').join('.'); } const rect = el.getBoundingClientRect(); const getSpacing = (prefix) => { const t = computed[prefix + 'Top']; const r = computed[prefix + 'Right']; const b = computed[prefix + 'Bottom']; const l = computed[prefix + 'Left']; if (t === r && r === b && b === l) return t; if (t === b && r === l) return `${t} ${r}`; return `${t} ${r} ${b} ${l}`; }; return { fontFamily: computed.fontFamily, fontSize: computed.fontSize, fontWeight: computed.fontWeight, fontStyle: computed.fontStyle, textAlign: computed.textAlign, color: computed.color, width: `${Math.round(rect.width)}px`, height: `${Math.round(rect.height)}px`, margin: getSpacing('margin'), padding: getSpacing('padding'), tag: tagStr }; } // --- Overlay Implementation (Runs in all frames) --- let overlayContainer; function getOverlay() { if (!overlayContainer) { overlayContainer = document.createElement('div'); overlayContainer.id = 'inspector-overlay-container'; // Use shadow DOM if possible to isolate styles, but regular div is fine for userscript overlayContainer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2147483646; display: none; `; // Inner HTML structure for Margin/Border/Padding/Content // We draw them as separate absolute divs overlayContainer.innerHTML = ` <div id="insp-highlight" style="position:absolute; box-sizing: border-box; display:none;"></div> <div id="insp-tag-label" style="position:absolute; padding: 0 4px; background: #d9534f; color: white; font-size: 10px; font-family: monospace; line-height: 16px; display:none;"></div> `; // Add Styles for overlay const style = document.createElement('style'); style.textContent = ` #insp-highlight { border: 1px dashed #f00; background: rgba(255, 0, 0, 0.05); /* Very faint red tint to indicate selection */ } `; document.head.appendChild(style); document.body.appendChild(overlayContainer); } return overlayContainer; } function updateOverlay(el) { const container = getOverlay(); container.style.display = 'block'; const computed = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); const mt = parseFloat(computed.marginTop) || 0; const mr = parseFloat(computed.marginRight) || 0; const mb = parseFloat(computed.marginBottom) || 0; const ml = parseFloat(computed.marginLeft) || 0; const pt = parseFloat(computed.paddingTop) || 0; const pr = parseFloat(computed.paddingRight) || 0; const pb = parseFloat(computed.paddingBottom) || 0; const pl = parseFloat(computed.paddingLeft) || 0; const bt = parseFloat(computed.borderTopWidth) || 0; const br = parseFloat(computed.borderRightWidth) || 0; const bb = parseFloat(computed.borderBottomWidth) || 0; const bl = parseFloat(computed.borderLeftWidth) || 0; // Draw Highlight Box (Matches the element's border-box) const highlight = container.querySelector('#insp-highlight'); highlight.style.display = 'block'; highlight.style.left = rect.left + 'px'; highlight.style.top = rect.top + 'px'; highlight.style.width = rect.width + 'px'; highlight.style.height = rect.height + 'px'; // Draw Label (Bottom Left of Margin Box usually, or bottom left of element) const label = container.querySelector('#insp-tag-label'); label.style.display = 'block'; let tagInfo = el.tagName.toLowerCase(); if (el.id) tagInfo += '#' + el.id; if (el.classList.length > 0) tagInfo += '.' + [...el.classList].join('.'); label.textContent = `${tagInfo} | ${Math.round(rect.width)} x ${Math.round(rect.height)}`; // Position below the element label.style.left = rect.left + 'px'; label.style.top = (rect.bottom + mt + 2) + 'px'; // Adjust if off screen if (rect.bottom + mt + 20 > window.innerHeight) { label.style.top = (rect.top - mt - 20) + 'px'; } } function hideOverlay() { if (overlayContainer) overlayContainer.style.display = 'none'; } function notifyHighlightActive() { if (isTop) { // If Top is highlighting, clear all children propagateClear(null); } else { // If Iframe is highlighting, tell Top to clear others window.top.postMessage({ type: 'STYLE_HIGHLIGHT_ACTIVE' }, '*'); } } // --- UI Construction (Top Window Only) --- let panel, toggleBtn, fields; if (isTop) { initPanel(); } else { // If I am an iframe, ask top for current state when I load // Retry a few times in case Top isn't ready const queryState = () => window.top.postMessage({ type: 'STYLE_QUERY_STATE' }, '*'); queryState(); setTimeout(queryState, 1000); } function initPanel() { // Create Panel panel = document.createElement('div'); panel.id = 'style-inspector-panel'; panel.innerHTML = ` <div class="inspector-header" id="inspector-drag-handle"> <span>Style Inspector</span> <div class="inspector-controls"> <button id="inspector-toggle">OFF</button> <button id="inspector-close">×</button> </div> </div> <div class="inspector-content"> <div class="inspector-item"><span class="label">Font Family:</span> <span id="val-font-family">-</span></div> <div class="inspector-item"><span class="label">Font Size:</span> <span id="val-font-size">-</span></div> <div class="inspector-item"><span class="label">Font Weight:</span> <span id="val-font-weight">-</span></div> <div class="inspector-item"><span class="label">Font Style:</span> <span id="val-font-style">-</span></div> <div class="inspector-item"><span class="label">Text Align:</span> <span id="val-text-align">-</span></div> <div class="inspector-item"><span class="label">Color:</span> <span id="val-color">-</span> <span id="color-preview"></span></div> <div class="inspector-item"><span class="label">Width:</span> <span id="val-width">-</span></div> <div class="inspector-item"><span class="label">Height:</span> <span id="val-height">-</span></div> <div class="inspector-item"><span class="label">Margin:</span> <span id="val-margin">-</span></div> <div class="inspector-item"><span class="label">Padding:</span> <span id="val-padding">-</span></div> <div class="inspector-item"><span class="label">Tag:</span> <span id="val-tag">-</span></div> </div> `; // Styles const css = ` #style-inspector-panel { position: fixed; bottom: 100px; right: 20px; width: 300px; background: rgba(30, 30, 30, 0.95); backdrop-filter: blur(4px); color: #eee; font-family: Consolas, Monaco, "Andale Mono", monospace; font-size: 12px; border-radius: 6px; z-index: 2147483647; padding: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.4); display: none; /* Hidden until loaded */ border: 1px solid #555; } .inspector-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #333; border-bottom: 1px solid #555; border-radius: 6px 6px 0 0; cursor: move; font-weight: bold; user-select: none; } .inspector-controls { display: flex; gap: 8px; } #inspector-toggle { background: #d9534f; border: none; color: white; padding: 2px 8px; border-radius: 3px; cursor: pointer; font-size: 11px; font-weight: bold; transition: background 0.2s; } #inspector-toggle.active { background: #5cb85c; } #inspector-close { background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 18px; line-height: 1; padding: 0 4px; } #inspector-close:hover { color: #fff; } .inspector-content { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; cursor: default; user-select: text; } .inspector-item { display: flex; align-items: flex-start; line-height: 1.4; } .inspector-item .label { color: #aaa; flex-shrink: 0; width: 85px; } .inspector-item span:not(.label) { flex: 1; word-break: break-all; color: #fff; } #color-preview { display: inline-block; width: 12px; height: 12px; border: 1px solid #777; margin-left: 6px; vertical-align: middle; } `; const styleSheet = document.createElement("style"); styleSheet.textContent = css; (document.head || document.documentElement).appendChild(styleSheet); // Append to body safely const appendUI = () => { if (document.body) { document.body.appendChild(panel); panel.style.display = 'block'; setupDraggable(panel, document.getElementById('inspector-drag-handle')); } else { requestAnimationFrame(appendUI); } }; appendUI(); // Bind Elements toggleBtn = panel.querySelector('#inspector-toggle'); fields = { fontFamily: panel.querySelector('#val-font-family'), fontSize: panel.querySelector('#val-font-size'), fontWeight: panel.querySelector('#val-font-weight'), fontStyle: panel.querySelector('#val-font-style'), textAlign: panel.querySelector('#val-text-align'), color: panel.querySelector('#val-color'), colorPreview: panel.querySelector('#color-preview'), width: panel.querySelector('#val-width'), height: panel.querySelector('#val-height'), margin: panel.querySelector('#val-margin'), padding: panel.querySelector('#val-padding'), tag: panel.querySelector('#val-tag') }; // Events toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); // Stop click from triggering inspector const newState = !isActive; setActive(newState); propagateState(newState); // Tell children }); panel.querySelector('#inspector-close').addEventListener('click', () => { panel.style.display = 'none'; setActive(false); propagateState(false); }); } function updateToggleStateUI() { if (!toggleBtn) return; if (isActive) { toggleBtn.textContent = 'ON'; toggleBtn.classList.add('active'); } else { toggleBtn.textContent = 'OFF'; toggleBtn.classList.remove('active'); } } function updatePanelUI(data) { if (!fields) return; fields.fontFamily.textContent = data.fontFamily; fields.fontSize.textContent = data.fontSize; fields.fontWeight.textContent = data.fontWeight; fields.fontStyle.textContent = data.fontStyle; fields.textAlign.textContent = data.textAlign; fields.color.textContent = data.color; fields.colorPreview.style.backgroundColor = data.color; fields.width.textContent = data.width; fields.height.textContent = data.height; fields.margin.textContent = data.margin; fields.padding.textContent = data.padding; fields.tag.textContent = data.tag; } // Draggable Implementation function setupDraggable(element, handle) { let isDragging = false; let startX, startY, initialLeft, initialTop; handle.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; // Remove bottom/right positioning to allow full free movement element.style.bottom = 'auto'; element.style.right = 'auto'; element.style.left = initialLeft + 'px'; element.style.top = initialTop + 'px'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; element.style.left = (initialLeft + dx) + 'px'; element.style.top = (initialTop + dy) + 'px'; } function onMouseUp() { isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } } })();