n8n Workflow Preview Enhancer

Fix the awful non-responsive layout on n8n workflow pages, kupo!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         n8n Workflow Preview Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Fix the awful non-responsive layout on n8n workflow pages, kupo!
// @author       You
// @match        https://n8n.io/workflows/*
// @exclude      https://n8n.io/workflows/
// @exclude      https://n8n.io/workflows
// @grant        none
// @run-at       document-idle
// @license     GPL-3.0-or-later

// ==/UserScript==

(function() {
    'use strict';

    console.log('n8n Layout Fix: Enhanced version with SPA navigation support, kupo!');

    let isFixing = false;
    let fixAttempts = 0;
    const maxFixAttempts = 15; // Increased for slow iframe loading

    // Function to find elements in shadow DOM
    const findInShadowDOM = (selector) => {
        const elements = [];

        // Find all elements with shadow roots
        const allElements = document.querySelectorAll('*');
        allElements.forEach(el => {
            if (el.shadowRoot) {
                const shadowElements = el.shadowRoot.querySelectorAll(selector);
                elements.push(...shadowElements);
            }
        });

        return elements;
    };

    // Enhanced function to wait for iframe content
    const waitForIframeContent = () => {
        return new Promise((resolve) => {
            const checkIframe = () => {
                const iframe = document.querySelector('#int_iframe, iframe.embedded_workflow_iframe');
                if (iframe) {
                    // Check if iframe has loaded content
                    try {
                        if (iframe.contentDocument || iframe.contentWindow) {
                            console.log('Iframe detected and accessible, kupo!');
                            resolve(true);
                            return;
                        }
                    } catch (e) {
                        // Cross-origin iframe, but that's normal for n8n
                        console.log('Cross-origin iframe detected (normal), kupo!');
                        resolve(true);
                        return;
                    }
                }

                // Also check for shadow DOM iframe
                const shadowIframe = findInShadowDOM('#int_iframe, .embedded_workflow_iframe');
                if (shadowIframe.length > 0) {
                    console.log('Shadow DOM iframe found, kupo!');
                    resolve(true);
                    return;
                }

                setTimeout(checkIframe, 200);
            };

            checkIframe();

            // Fallback timeout
            setTimeout(() => resolve(false), 8000);
        });
    };

    // Function to apply all layout fixes
    const fixLayout = async () => {
        if (isFixing) return false;
        isFixing = true;

        console.log(`Fix attempt ${fixAttempts + 1}/${maxFixAttempts}, kupo!`);

        let fixesApplied = 0;

        // Fix 1: Change flex-direction to column
        const targetElement = document.querySelector('.section-content-group.flex.flex-col.gap-8.lg\\:flex-row');
        if (targetElement) {
            targetElement.style.flexDirection = 'column !important';
            targetElement.classList.remove('lg:flex-row');
            console.log('Flex-direction fix applied, kupo!');
            fixesApplied++;
        }

        // Fix 2: Add max-width calc(100vw - 55px) to BOTH outer wrapper variations
        const maxWidthSelectors = [
            'div.max-w-section-default.mx-auto.w-full',
            'div.mx-auto.w-full.max-w-section-default'
        ];

        maxWidthSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(el => {
                el.style.maxWidth = 'calc(100vw - 55px)';
                console.log('Max-width calc(100vw - 55px) applied to: ' + selector + ', kupo!');
                fixesApplied++;
            });
        });

        // Fix 3: Update width calc(100vw - 55px) to lg:w-8/12 element
        const targetWidthElement = document.querySelector('div.lg\\:w-8\\/12');
        if (targetWidthElement) {
            targetWidthElement.style.width = 'calc(100vw - 55px)';
            console.log('Width calc(100vw - 55px) applied to lg:w-8/12 element, kupo!');
            fixesApplied++;
        }


        // Wait for iframe content before applying iframe-related fixes
        console.log('Waiting for iframe content to load, kupo...');
        await waitForIframeContent();

        // Fix 4: Regular DOM elements with min-height 100vh
        const regularSelectors = [
            'div.base-frame.relative.rounded-2xl.bg-white.bg-opacity-10.p-2.base-frame--default.workflow-viewer[data-v-57c68cc9][data-v-2f4878b1][data-v-6c0d4504]',
            'div.base-frame-inner.overflow-hidden.rounded-xl[data-v-57c68cc9]',
            'n8n-demo[data-v-2f4878b1]'
        ];

        regularSelectors.forEach(selector => {
            const el = document.querySelector(selector);
            if (el) {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to ' + selector + ', kupo!');
                fixesApplied++;
            }
        });

        // Fix 5: The bg-white div (try different approach)
        const bgWhiteElements = document.querySelectorAll('div[data-v-2f4878b1]');
        bgWhiteElements.forEach(el => {
            if (el.classList.contains('bg-white')) {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to bg-white div, kupo!');
                fixesApplied++;
            }
        });

        // Fix 6: Shadow DOM elements (with retry mechanism)
        const shadowSelectors = [
            '.embedded_workflow',
            '.canvas-container',
            '#int_iframe.embedded_workflow_iframe.non_interactive'
        ];

        shadowSelectors.forEach(selector => {
            const shadowElements = findInShadowDOM(selector);
            shadowElements.forEach(el => {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to shadow DOM element: ' + selector + ', kupo!');
                fixesApplied++;
            });
        });

        // Alternative approach for shadow DOM - find n8n-demo and access its shadow root
        const n8nDemo = document.querySelector('n8n-demo[data-v-2f4878b1]');
        if (n8nDemo && n8nDemo.shadowRoot) {
            const shadowRoot = n8nDemo.shadowRoot;

            // Try to find elements in this specific shadow root
            const embeddedWorkflow = shadowRoot.querySelector('.embedded_workflow');
            if (embeddedWorkflow) {
                embeddedWorkflow.style.minHeight = '100vh';
                console.log('Applied 100vh to .embedded_workflow in shadow root, kupo!');
                fixesApplied++;
            }

            const canvasContainer = shadowRoot.querySelector('.canvas-container');
            if (canvasContainer) {
                canvasContainer.style.minHeight = '100vh';
                console.log('Applied 100vh to .canvas-container in shadow root, kupo!');
                fixesApplied++;
            }

            const iframe = shadowRoot.querySelector('#int_iframe');
            if (iframe) {
                iframe.style.minHeight = '100vh';
                console.log('Applied 100vh to iframe in shadow root, kupo!');
                fixesApplied++;
            }
        }

        isFixing = false;
        return fixesApplied > 0;
    };

    // Function to detect URL changes (for SPA navigation)
    const detectURLChange = () => {
        let currentURL = window.location.href;

        const checkURLChange = () => {
            if (window.location.href !== currentURL) {
                currentURL = window.location.href;
                console.log('URL changed to:', currentURL, 'kupo!');

                // Reset fix attempts for new page
                fixAttempts = 0;

                // Wait a bit for the new content to start loading
                setTimeout(() => {
                    console.log('Starting layout fixes after navigation, kupo!');
                    startFixingProcess();
                }, 500);
            }
        };

        setInterval(checkURLChange, 100);
    };

    // Main fixing process with enhanced retry logic
    const startFixingProcess = async () => {
        // Try to fix immediately
        if (await fixLayout()) {
            console.log('Initial layout fixes applied successfully, kupo!');
        }

        // Enhanced retry mechanism
        const retryInterval = setInterval(async () => {
            fixAttempts++;

            if (fixAttempts >= maxFixAttempts) {
                clearInterval(retryInterval);
                console.log('Max fix attempts reached. Layout fix monitoring complete, kupo!');
                return;
            }

            await fixLayout();
        }, 1000); // Check every second

        // Watch for dynamic content changes
        const observer = new MutationObserver(async (mutations) => {
            let shouldCheck = false;

            mutations.forEach(mutation => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Check if any added nodes contain our target elements
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) { // Element node
                            if (node.matches && (
                                node.matches('n8n-demo') ||
                                node.matches('.base-frame') ||
                                node.matches('iframe') ||
                                node.querySelector && (
                                    node.querySelector('n8n-demo') ||
                                    node.querySelector('.base-frame') ||
                                    node.querySelector('iframe')
                                )
                            )) {
                                shouldCheck = true;
                            }
                        }
                    });
                }
            });

            if (shouldCheck && fixAttempts < maxFixAttempts) {
                setTimeout(async () => {
                    console.log('DOM change detected, applying fixes, kupo!');
                    await fixLayout();
                }, 300);
            }
        });

        // Start observing with enhanced options
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false, // Don't watch attributes to reduce noise
            attributeOldValue: false
        });
    };

    // Start the URL change detection for SPA navigation
    detectURLChange();

    // Start the initial fixing process
    startFixingProcess();

})();