n8n Workflow Preview Enhancer

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();