Style Inspector

Intercept click events to display element style info in a draggable floating panel. One global panel supports iframes. Toggle with Ctrl+F1.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
        }
    }

})();