Style Inspector

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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

})();