kiranwayne

GitHub Enhanced v1.7.0

May 31st, 2025
26
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 26.18 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         GitHub Enhanced
  3. // @namespace    http://tampermonkey.net/
  4. // @version      1.7.0
  5. // @description  Customize content width on GitHub pages with a slider. Defaults to GitHub's width. Incorporates layout adjustments. Fixes width input.
  6. // @author       kiranwayne
  7. // @match        https://github.com/*
  8. // @match        https://gist.github.com/*
  9. // @grant        GM_getValue
  10. // @grant        GM_setValue
  11. // @grant        GM_registerMenuCommand
  12. // @grant        GM_unregisterMenuCommand
  13. // @run-at       document-end
  14. // ==/UserScript==
  15.  
  16. (async () => {
  17.     'use strict';
  18.  
  19.     // --- Configuration & Constants ---
  20.     const SCRIPT_NAME = 'GitHub Enhanced';
  21.     const SCRIPT_VERSION = '1.7.0';
  22.     const SCRIPT_AUTHOR = 'kiranwayne';
  23.  
  24.     const CONFIG_PREFIX = 'githubEnhancedWidth_v1_'; // Prefix for GM_setValue keys
  25.     const MAX_WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx'; // Stores the user's preferred custom max width
  26.     const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth'; // Boolean: true if script should not alter width
  27.     const UI_VISIBLE_KEY = CONFIG_PREFIX + 'uiVisible'; // Boolean: true if settings panel is shown
  28.  
  29.     const WIDTH_STYLE_ID = 'gh-enhanced-width-style'; // ID for the style tag applying custom max-width
  30.     const LAYOUT_ADJUSTMENTS_STYLE_ID = 'gh-enhanced-layout-adjustments-style'; // ID for style tag with layout fixes
  31.     const GLOBAL_STYLE_ID = 'gh-enhanced-global-style'; // ID for global styles (e.g., for the settings panel)
  32.     const SETTINGS_PANEL_ID = 'gh-userscript-settings-panel'; // ID for the settings panel element
  33.  
  34.     // CSS selector for elements whose max-width will be controlled by the script
  35.     const WIDTH_TARGET_SELECTOR = `
  36.         .application-main .container-xl,
  37.         .application-main .container-lg,
  38.         .container-xl,
  39.         .container-lg,
  40.         .feed-content > .Details > .container-lg,
  41.         .application-main div[data-target="react-app.reactRoot"] div[class^='prc-PageLayout-Content-'] > div[class^='Box-sc-'],
  42.         .application-main div[data-target="react-app.reactRoot"] > div[class^='Box-sc-'] > div[class^='Box-sc-'] > div[class^='Box-sc-'] > div[class^='Box-sc-'],
  43.         .application-main div[data-target="react-app.reactRoot"] div[class^='IssueCreatePage-module__createPaneContainer-'],
  44.         .application-main div[style^="--sticky-pane-height:"] > div[class^='Box-sc-'] > div[class^='Box-sc-'] > div[class^='Box-sc-'] > div[class^='Box-sc-']:nth-child(2) > div[class^='Box-sc-'],
  45.         #js-repo-pjax-container div[style^="--sticky-pane-height:"] > div[class^='Box-sc-']:first-child,
  46.         .gist-content .container-lg
  47.     `.split(',').map(s => s.trim()).filter(s => s).join(', ');
  48.  
  49.     // Default value for the *custom* width slider if the user has never set one.
  50.     // This is NOT the script's initial active width; initially, script uses GitHub's default.
  51.     const SCRIPT_DEFAULT_CUSTOM_WIDTH_PX = 1600;
  52.     const MIN_WIDTH_PX = 1500;    // Minimum settable custom width
  53.     const MAX_WIDTH_PX = 3300;   // Maximum settable custom width
  54.     const STEP_WIDTH_PX = 20;    // Increment/decrement step for slider and number input
  55.  
  56.     // Holds the current configuration. Loaded from GM storage.
  57.     let config = {
  58.         maxWidthPx: SCRIPT_DEFAULT_CUSTOM_WIDTH_PX, // Fallback, overwritten by loadSettings
  59.         useDefaultWidth: true, // Script defaults to not altering GitHub's width
  60.         uiVisible: false,      // Settings panel hidden by default
  61.     };
  62.  
  63.     // DOM element references for the settings panel
  64.     let settingsPanel = null, widthSlider = null, widthLabel = null, widthInput = null;
  65.     let defaultWidthCheckbox = null;
  66.     let menuCommandId_ToggleUI = null; // ID for the Tampermonkey menu command
  67.     const allStyleRoots = new Set(); // Set of document.head and any discovered Shadow DOM roots
  68.  
  69.     // --- Helper Functions ---
  70.  
  71.     /**
  72.      * Loads settings from GM storage into the global `config` object.
  73.      * Defaults are applied if settings are not found.
  74.      */
  75.     async function loadSettings() {
  76.         config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, true);
  77.         config.maxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_CUSTOM_WIDTH_PX);
  78.         config.maxWidthPx = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, config.maxWidthPx)); // Clamp
  79.         config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false);
  80.     }
  81.  
  82.     /**
  83.      * Saves a specific setting to GM storage and updates the global `config` object.
  84.      * @param {string} key - The GM storage key (e.g., MAX_WIDTH_PX_KEY).
  85.      * @param {*} value - The value to save.
  86.      */
  87.     async function saveSetting(key, value) {
  88.         if (key === MAX_WIDTH_PX_KEY) {
  89.             const numValue = parseInt(value, 10);
  90.             if (!isNaN(numValue)) {
  91.                 const clampedValue = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, numValue));
  92.                 await GM_setValue(key, clampedValue);
  93.                 config.maxWidthPx = clampedValue; // Ensure internal config is also clamped
  94.             } else { return; } // Do not save if not a valid number
  95.         } else {
  96.             // For boolean settings or other types
  97.             await GM_setValue(key, value);
  98.             if (key === USE_DEFAULT_WIDTH_KEY) config.useDefaultWidth = value;
  99.             else if (key === UI_VISIBLE_KEY) config.uiVisible = value;
  100.         }
  101.     }
  102.  
  103.     // --- Style Generation Functions ---
  104.  
  105.     /**
  106.      * Generates CSS to apply the custom maximum width.
  107.      * Returns an empty string if `config.useDefaultWidth` is true.
  108.      */
  109.     function getCustomWidthConstraintCss() {
  110.         if (config.useDefaultWidth) return '';
  111.         return `${WIDTH_TARGET_SELECTOR} { max-width: ${config.maxWidthPx}px !important; margin-left: auto !important; margin-right: auto !important; }`;
  112.     }
  113.  
  114.     /**
  115.      * Generates CSS for layout adjustments (e.g., making sidebars use full width).
  116.      * Inspired by the original "Wide GitHub" script.
  117.      * Returns an empty string if `config.useDefaultWidth` is true.
  118.      */
  119.     function getLayoutAdjustmentsCss() {
  120.         if (config.useDefaultWidth) return '';
  121.         return `
  122.             /* Make repository overview sidebar (e.g., "About") use available right space */
  123.             .react-repos-overview-margin { margin-right: 0 !important; }
  124.             /* Ensure specific columns (e.g., on Tags page) can use full width */
  125.             .application-main .col-11 { width: 100% !important; }
  126.             /* Fix alignment of assignees in issue lists at wider views */
  127.             #js-repo-pjax-container .js-issue-row .text-right { max-width: 303px !important; }
  128.         `;
  129.     }
  130.  
  131.     /**
  132.      * Generates CSS for the settings panel.
  133.      */
  134.     function getGlobalPanelCss() {
  135.         return `
  136.             #${SETTINGS_PANEL_ID} { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 14px; line-height: 1.5; }
  137.             #${SETTINGS_PANEL_ID} input[type=number] { -moz-appearance: textfield !important; } /* Firefox: remove spinners */
  138.             #${SETTINGS_PANEL_ID} input[type=number]::-webkit-inner-spin-button,
  139.             #${SETTINGS_PANEL_ID} input[type=number]::-webkit-outer-spin-button { -webkit-appearance: inner-spin-button !important; opacity: 1 !important; cursor: pointer; } /* Webkit: ensure spinners are visible and styled if browser shows them */
  140.             #${SETTINGS_PANEL_ID} { position: fixed; top: 10px; right: 10px; z-index: 99999; background-color: var(--color-canvas-default, #ffffff); color: var(--color-fg-default, #24292f); border: 1px solid var(--color-border-default, #d0d7de); border-radius: 6px; padding: 16px; box-shadow: var(--color-shadow-large, 0 8px 24px rgba(140,149,159,0.2)); min-width: 300px; }
  141.             #${SETTINGS_PANEL_ID} h4 { margin-top: 0; margin-bottom: 8px; font-size: 16px; font-weight: 600; padding-bottom: 8px; border-bottom: 1px solid var(--color-border-muted, #d8dee4); }
  142.             #${SETTINGS_PANEL_ID} p { margin-top:0; margin-bottom: 4px; font-size: 12px; opacity: 0.8; }
  143.             #${SETTINGS_PANEL_ID} label { font-weight: 400; cursor: pointer; vertical-align: middle; }
  144.             #${SETTINGS_PANEL_ID} input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
  145.             #${SETTINGS_PANEL_ID} .control-group { margin-bottom: 15px; }
  146.             #${SETTINGS_PANEL_ID} .width-controls-wrapper { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
  147.             #${SETTINGS_PANEL_ID} input[type="range"] { flex-grow: 1; vertical-align: middle; }
  148.             #${SETTINGS_PANEL_ID} input[type="number"] { width: 70px; padding: 5px 8px; background-color: var(--color-input-bg, var(--color-canvas-subtle, #f6f8fa)); color: var(--color-fg-default, #24292f); border: 1px solid var(--color-border-default, #d0d7de); border-radius: 6px; vertical-align: middle; }
  149.             #${SETTINGS_PANEL_ID} .width-value-label { min-width: 55px; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; text-align: right; vertical-align: middle; }
  150.         `;
  151.     }
  152.  
  153.     // --- Style Injection / Update / Removal Functions ---
  154.  
  155.     /**
  156.      * Injects or updates a <style> tag with given CSS content into a root node.
  157.      * Handles both document.head and Shadow DOM roots.
  158.      * Removes the style tag if cssContent is empty or null.
  159.      * @param {Node} root - The root node (e.g., document.head or a ShadowRoot).
  160.      * @param {string} styleId - The ID for the <style> tag.
  161.      * @param {string} cssContent - The CSS content.
  162.      */
  163.     function injectOrUpdateStyle(root, styleId, cssContent) {
  164.         if (!root) return;
  165.         let style = root.querySelector(`#${styleId}`);
  166.         if (cssContent && cssContent.trim() !== '') { // Only add/update if there's content
  167.             if (!style) {
  168.                 style = document.createElement('style'); style.id = styleId; style.textContent = cssContent;
  169.                 // Append to document.head or a ShadowRoot
  170.                 if (root === document.head || (root.nodeType === Node.ELEMENT_NODE && root.shadowRoot === null) || root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) root.appendChild(style);
  171.                 else if (root.shadowRoot) root.shadowRoot.appendChild(style);
  172.             } else if (style.textContent !== cssContent) {
  173.                 style.textContent = cssContent; // Update existing style
  174.             }
  175.         } else { // If cssContent is empty, remove the style tag
  176.             if (style) style.remove();
  177.         }
  178.     }
  179.  
  180.     /** Applies global CSS (for settings panel) to document.head. */
  181.     function applyGlobalHeadStyles() {
  182.         if (document.head) injectOrUpdateStyle(document.head, GLOBAL_STYLE_ID, getGlobalPanelCss());
  183.     }
  184.  
  185.     /** Applies custom width CSS to all known style roots. */
  186.     function applyCustomWidthConstraintStyleToAllRoots() {
  187.         const css = getCustomWidthConstraintCss();
  188.         allStyleRoots.forEach(r => { if(r) injectOrUpdateStyle(r, WIDTH_STYLE_ID, css); });
  189.     }
  190.  
  191.     /** Applies layout adjustment CSS to all known style roots. */
  192.     function applyLayoutAdjustmentsStyleToAllRoots() {
  193.         const css = getLayoutAdjustmentsCss();
  194.         allStyleRoots.forEach(r => { if(r) injectOrUpdateStyle(r, LAYOUT_ADJUSTMENTS_STYLE_ID, css); });
  195.     }
  196.  
  197.     // --- UI State Update ---
  198.  
  199.     /** Updates the settings panel UI elements to reflect the current `config`. */
  200.     function updateUIState() {
  201.         if (settingsPanel && defaultWidthCheckbox && widthSlider && widthLabel && widthInput) {
  202.             defaultWidthCheckbox.checked = config.useDefaultWidth;
  203.             const isCustomWidthEnabled = !config.useDefaultWidth;
  204.             // Disable/enable width controls based on "Use GitHub Default Width"
  205.             [widthSlider, widthInput].forEach(el => el.disabled = !isCustomWidthEnabled);
  206.             const opacityValue = isCustomWidthEnabled ? 1 : 0.5; // Dim controls if disabled
  207.             [widthLabel, widthSlider, widthInput].forEach(el => el.style.opacity = opacityValue);
  208.             // Set slider, input, and label to the stored custom max width
  209.             widthSlider.value = config.maxWidthPx;
  210.             widthInput.value = config.maxWidthPx;
  211.             widthLabel.textContent = `${config.maxWidthPx}px`;
  212.         }
  213.     }
  214.  
  215.     /** Handles clicks outside the settings panel to close it. */
  216.     async function handleClickOutside(event) {
  217.         if (settingsPanel && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target) && config.uiVisible) {
  218.             await saveSetting(UI_VISIBLE_KEY, false); // Set panel to not visible
  219.             removeSettingsUI();
  220.             updateTampermonkeyMenu(); // Update menu item text
  221.         }
  222.     }
  223.  
  224.     /** Removes the settings panel UI from the DOM. */
  225.     function removeSettingsUI() {
  226.         if (document) document.removeEventListener('click', handleClickOutside, true);
  227.         settingsPanel = document.getElementById(SETTINGS_PANEL_ID);
  228.         if (settingsPanel) {
  229.             settingsPanel.remove();
  230.             // Clear references to panel elements
  231.             settingsPanel = null; widthSlider = null; widthLabel = null; widthInput = null; defaultWidthCheckbox = null;
  232.         }
  233.     }
  234.  
  235.     /** Creates and appends the settings panel UI to the DOM. */
  236.     function createSettingsUI() {
  237.         if (document.getElementById(SETTINGS_PANEL_ID) || !config.uiVisible) return; // Don't create if exists or not visible
  238.         if (!document.body) { console.warn(`[${SCRIPT_NAME}] document.body not found, cannot create Settings UI.`); return; }
  239.  
  240.         settingsPanel = document.createElement('div'); settingsPanel.id = SETTINGS_PANEL_ID;
  241.         // Header section with script name, version, author
  242.         const headerDiv = document.createElement('div');
  243.         const titleElement = document.createElement('h4'); titleElement.textContent = SCRIPT_NAME;
  244.         const versionElement = document.createElement('p'); versionElement.textContent = `Version: ${SCRIPT_VERSION}`;
  245.         const authorElement = document.createElement('p'); authorElement.textContent = `Author: ${SCRIPT_AUTHOR}`;
  246.         headerDiv.append(titleElement, versionElement, authorElement);
  247.  
  248.         // Width control section
  249.         const widthSection = document.createElement('div'); widthSection.className = 'control-group'; widthSection.style.marginTop = '15px';
  250.         // "Use GitHub Default Width" checkbox
  251.         const defaultWidthDiv = document.createElement('div'); defaultWidthDiv.style.marginBottom = '10px';
  252.         defaultWidthCheckbox = document.createElement('input'); defaultWidthCheckbox.type = 'checkbox'; defaultWidthCheckbox.id = 'gh-userscript-defaultwidth-toggle';
  253.         const defaultWidthLabelElement = document.createElement('label'); defaultWidthLabelElement.htmlFor = 'gh-userscript-defaultwidth-toggle'; defaultWidthLabelElement.textContent = ' Use GitHub Default Width';
  254.         defaultWidthDiv.append(defaultWidthCheckbox, defaultWidthLabelElement);
  255.         // Custom width controls (slider, number input, label)
  256.         const customWidthControlsDiv = document.createElement('div'); customWidthControlsDiv.className = 'width-controls-wrapper';
  257.         widthLabel = document.createElement('span'); widthLabel.className = 'width-value-label';
  258.         widthSlider = document.createElement('input'); widthSlider.type = 'range'; widthSlider.min = MIN_WIDTH_PX; widthSlider.max = MAX_WIDTH_PX; widthSlider.step = STEP_WIDTH_PX;
  259.         widthInput = document.createElement('input'); widthInput.type = 'number'; widthInput.min = MIN_WIDTH_PX; widthInput.max = MAX_WIDTH_PX; widthInput.step = STEP_WIDTH_PX; // HTML5 number input handles step validation
  260.         customWidthControlsDiv.append(widthLabel, widthSlider, widthInput);
  261.  
  262.         widthSection.append(defaultWidthDiv, customWidthControlsDiv);
  263.         settingsPanel.append(headerDiv, widthSection);
  264.         document.body.appendChild(settingsPanel);
  265.  
  266.         // Event listener for "Use GitHub Default Width" checkbox
  267.         defaultWidthCheckbox.addEventListener('change', async (e) => {
  268.             await saveSetting(USE_DEFAULT_WIDTH_KEY, e.target.checked);
  269.             // If user unchecks for the first time in a session, ensure config.maxWidthPx is up-to-date
  270.             if (!config.useDefaultWidth && config.maxWidthPx === SCRIPT_DEFAULT_CUSTOM_WIDTH_PX) {
  271.                  config.maxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_CUSTOM_WIDTH_PX);
  272.                  config.maxWidthPx = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, config.maxWidthPx));
  273.             }
  274.             applyCustomWidthConstraintStyleToAllRoots();
  275.             applyLayoutAdjustmentsStyleToAllRoots();
  276.             updateUIState(); // Reflect changes in UI (disabled state, values)
  277.         });
  278.  
  279.         // Event listener for width slider (live input)
  280.         widthSlider.addEventListener('input', (e) => {
  281.             const nw = parseInt(e.target.value, 10);
  282.             config.maxWidthPx = nw; // Update live config (slider value is inherently clamped by its min/max)
  283.             if (widthLabel) widthLabel.textContent = `${nw}px`;
  284.             if (widthInput) widthInput.value = nw; // Sync number input
  285.             if (!config.useDefaultWidth) applyCustomWidthConstraintStyleToAllRoots(); // Apply style live
  286.         });
  287.         // Event listener for width slider (on change - typically mouse up)
  288.         widthSlider.addEventListener('change', async (e) => {
  289.             const valueToSave = parseInt(e.target.value, 10);
  290.             if (!config.useDefaultWidth) {
  291.                 await saveSetting(MAX_WIDTH_PX_KEY, valueToSave);
  292.             } else { // Even if default width is active, save the preferred custom width
  293.                 await GM_setValue(MAX_WIDTH_PX_KEY, valueToSave);
  294.             }
  295.         });
  296.  
  297.         // Event listener for width number input (live input)
  298.         widthInput.addEventListener('input', (e) => {
  299.             let rawValue = e.target.value;
  300.             if (rawValue === '') { // If input is cleared, show current actual value in label
  301.                 if (widthLabel) widthLabel.textContent = `${config.maxWidthPx}px`;
  302.                 return;
  303.             }
  304.             let nw = parseInt(rawValue, 10);
  305.             if (isNaN(nw)) return; // Do nothing if not a number
  306.  
  307.             if (widthLabel) widthLabel.textContent = `${nw}px`; // Show typed value in label
  308.             // Sync slider, clamping value for slider's own internal state
  309.             if (widthSlider) {
  310.                 if (nw >= MIN_WIDTH_PX && nw <= MAX_WIDTH_PX) widthSlider.value = nw;
  311.                 else if (nw < MIN_WIDTH_PX) widthSlider.value = MIN_WIDTH_PX;
  312.                 else widthSlider.value = MAX_WIDTH_PX;
  313.             }
  314.             // Apply potentially un-clamped value for immediate visual feedback if custom width is active
  315.             if (!config.useDefaultWidth) {
  316.                 const tempCss = `${WIDTH_TARGET_SELECTOR} { max-width: ${nw}px !important; margin-left: auto !important; margin-right: auto !important; }`;
  317.                 allStyleRoots.forEach(r => { if(r) injectOrUpdateStyle(r, WIDTH_STYLE_ID, tempCss); });
  318.             }
  319.         });
  320.         // Event listener for width number input (on change - blur or Enter)
  321.         widthInput.addEventListener('change', async (e) => {
  322.             let rawValue = e.target.value; let fw;
  323.             if (rawValue === '') fw = config.maxWidthPx; // If cleared, revert to current config value
  324.             else fw = parseInt(rawValue, 10);
  325.  
  326.             if (isNaN(fw)) fw = config.maxWidthPx; // If invalid text, revert
  327.             fw = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, fw)); // Clamp final value
  328.  
  329.             // Update UI elements to the final clamped value
  330.             e.target.value = fw;
  331.             config.maxWidthPx = fw; // Finalize config.maxWidthPx
  332.             if (widthLabel) widthLabel.textContent = `${fw}px`;
  333.             if (widthSlider) widthSlider.value = fw;
  334.  
  335.             // Save and apply the final clamped value
  336.             if (!config.useDefaultWidth) {
  337.                 await saveSetting(MAX_WIDTH_PX_KEY, fw);
  338.                 applyCustomWidthConstraintStyleToAllRoots(); // Apply final clamped style
  339.             } else { // Even if default width is active, save the preferred custom width
  340.                 await GM_setValue(MAX_WIDTH_PX_KEY, fw);
  341.             }
  342.         });
  343.  
  344.         updateUIState(); // Initialize UI element states
  345.         if (document) document.addEventListener('click', handleClickOutside, true); // For closing panel
  346.         applyGlobalHeadStyles(); // Ensure panel styles are present
  347.     }
  348.  
  349.     /** Updates or registers the Tampermonkey menu command to toggle the settings panel. */
  350.     function updateTampermonkeyMenu() {
  351.         if (menuCommandId_ToggleUI !== null && typeof GM_unregisterMenuCommand === 'function') {
  352.             try { GM_unregisterMenuCommand(menuCommandId_ToggleUI); } catch (e) { /* ignore */ }
  353.             menuCommandId_ToggleUI = null;
  354.         }
  355.         if (typeof GM_registerMenuCommand === 'function') {
  356.             const labelUI = config.uiVisible ? 'Hide Width Settings Panel' : 'Show Width Settings Panel';
  357.             menuCommandId_ToggleUI = GM_registerMenuCommand(labelUI, async () => {
  358.                 const newState = !config.uiVisible;
  359.                 await saveSetting(UI_VISIBLE_KEY, newState);
  360.                 if (newState) createSettingsUI(); else removeSettingsUI();
  361.                 updateTampermonkeyMenu(); // Update label for next time
  362.             });
  363.         }
  364.     }
  365.  
  366.     // --- Shadow DOM Handling ---
  367.     function getShadowRoot(element) { try { return element.shadowRoot; } catch (e) { return null; } }
  368.  
  369.     /** Processes an element to find its Shadow DOM root and add it to `allStyleRoots`. */
  370.     function processElementForShadowDOM(element) {
  371.         const shadow = getShadowRoot(element);
  372.         if (shadow && shadow.nodeType === Node.DOCUMENT_FRAGMENT_NODE && !allStyleRoots.has(shadow)) {
  373.             allStyleRoots.add(shadow);
  374.             // Apply current styles to the newly discovered shadow root
  375.             injectOrUpdateStyle(shadow, WIDTH_STYLE_ID, getCustomWidthConstraintCss());
  376.             injectOrUpdateStyle(shadow, LAYOUT_ADJUSTMENTS_STYLE_ID, getLayoutAdjustmentsCss());
  377.             return true;
  378.         }
  379.         return false;
  380.     }
  381.  
  382.     /** Scans the current document for any existing elements with Shadow DOM. */
  383.     function scanExistingShadowDOM() {
  384.         try {
  385.             document.querySelectorAll('*').forEach(el => {
  386.                 processElementForShadowDOM(el);
  387.                 // Also check children within a shadow root, if any (for nested custom elements)
  388.                 if (el.shadowRoot) el.shadowRoot.querySelectorAll('*').forEach(childEl => processElementForShadowDOM(childEl));
  389.             });
  390.         } catch (e) { console.error(`[${SCRIPT_NAME}] Error during initial Shadow DOM scan:`, e); }
  391.     }
  392.  
  393.     /** Starts a MutationObserver to detect dynamically added elements with Shadow DOM. */
  394.     function startDynamicShadowDOMObserver() {
  395.         const observer = new MutationObserver(mutationsList => {
  396.             for (const mutation of mutationsList) {
  397.                 if (mutation.type === 'childList') {
  398.                     mutation.addedNodes.forEach(node => {
  399.                         if (node.nodeType === Node.ELEMENT_NODE) {
  400.                             processElementForShadowDOM(node); // Check the new node
  401.                             node.querySelectorAll('*').forEach(el => processElementForShadowDOM(el)); // And its descendants
  402.                         }
  403.                     });
  404.                 }
  405.             }
  406.         });
  407.         if(document.body) observer.observe(document.body, { childList: true, subtree: true });
  408.         else document.addEventListener('DOMContentLoaded', () => { // Fallback if body not ready
  409.             if(document.body) observer.observe(document.body, { childList: true, subtree: true });
  410.         });
  411.     }
  412.  
  413.     // --- Initialization ---
  414.     console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} by ${SCRIPT_AUTHOR} starting...`);
  415.  
  416.     // Add document.head to style roots (or documentElement as fallback)
  417.     if (document.head) allStyleRoots.add(document.head);
  418.     else { const rootNode = document.documentElement || document; allStyleRoots.add(rootNode); console.warn(`[${SCRIPT_NAME}] document.head not found at init, using documentElement.`); }
  419.  
  420.     await loadSettings(); // Load settings from GM storage
  421.  
  422.     // Apply styles based on loaded configuration
  423.     applyGlobalHeadStyles(); // For the settings panel
  424.     applyCustomWidthConstraintStyleToAllRoots(); // Custom width or nothing if default
  425.     applyLayoutAdjustmentsStyleToAllRoots(); // Layout fixes or nothing if default
  426.  
  427.     scanExistingShadowDOM();       // Find any shadow roots already on the page
  428.     startDynamicShadowDOMObserver(); // Watch for new shadow roots
  429.  
  430.     // Create settings panel if it was configured to be visible
  431.     if (config.uiVisible) setTimeout(createSettingsUI, 100); // Delay slightly for page load
  432.  
  433.     updateTampermonkeyMenu(); // Register menu command
  434.  
  435.     // Handle GitHub's Turbo Drive navigation
  436.     document.addEventListener("turbo:load", async () => {
  437.         console.log(`[${SCRIPT_NAME}] Turbo:load event detected. Re-initializing styles and panel.`);
  438.         // Clear and re-populate style roots for the new page content
  439.         allStyleRoots.clear();
  440.         if(document.head) allStyleRoots.add(document.head); else allStyleRoots.add(document.documentElement||document);
  441.  
  442.         await loadSettings(); // Re-load settings in case they were changed in another tab
  443.  
  444.         // Re-apply styles based on potentially updated settings
  445.         applyCustomWidthConstraintStyleToAllRoots();
  446.         applyLayoutAdjustmentsStyleToAllRoots();
  447.         scanExistingShadowDOM(); // Scan new page content for shadow DOM
  448.  
  449.         // Re-create or remove settings panel based on current visibility config
  450.         const existingPanel = document.getElementById(SETTINGS_PANEL_ID);
  451.         if(config.uiVisible && !existingPanel) setTimeout(createSettingsUI, 100);
  452.         else if(!config.uiVisible && existingPanel) removeSettingsUI();
  453.     });
  454.  
  455.     console.log(`[${SCRIPT_NAME}] Initialization complete. Initial useDefaultWidth: ${config.useDefaultWidth}, CustomMaxWidth: ${config.maxWidthPx}`);
  456. })();
Advertisement
Add Comment
Please, Sign In to add comment