uy_uy

mes_textarea & reasoning_textarea styled

Jul 22nd, 2025
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 19.57 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         Styled Dynamic Textarea Overlays - Ultimate True
  3. // @namespace    http://tampermonkey.net/
  4. // @version      0.5 // Incrementing version for precise insertion before form_sheld
  5. // @description  Overlays styled, non-editable text areas over specific dynamic textareas by class, handles inner scrolls. Inserts overlays before form_sheld.
  6. // @author       Gemini
  7. // @match        *://*/*
  8. // @grant        none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12.     'use strict';
  13.  
  14.     // Set of target textarea CLASS NAMES
  15.     const TARGET_TEXTAREA_CLASSES = new Set(['reasoning_edit_textarea', 'edit_textarea']);
  16.     const SHELD_ID = 'sheld'; // The common parent for chat and form_sheld, and the new parent for overlays
  17.     const FORM_SHELD_ID = 'form_sheld'; // The element to insert overlays before
  18.  
  19.     // Map to store managed textarea elements and their associated overlay data
  20.     // Key: textareaElement, Value: { overlayDiv, observer, updateFunction, debouncedUpdateFunction, scrollListeners: [{element, handler}] }
  21.     const managedTextareas = new Map();
  22.  
  23.     // Helper for debouncing function calls
  24.     function debounce(func, delay) {
  25.         let timeout;
  26.         return function(...args) {
  27.             const context = this;
  28.             clearTimeout(timeout);
  29.             timeout = setTimeout(() => func.apply(context, args), delay);
  30.         };
  31.     }
  32.  
  33.     // Function to apply styling based on markdown-like syntax
  34.     function applyStyling(text) {
  35.         let styledText = text
  36.             .replace(/&/g, '&')
  37.             .replace(/</g, '&lt;')
  38.             .replace(/>/g, '&gt;')
  39.             .replace(/"(.*?)"/g, '<span style="color: #e0a331;">"$1"</span>') // Dialogue text (light blue)
  40.             .replace(/\*\*(.*?)\*\*/g, '<span style="font-weight: bold;">**$1**</span>') // Bold, keep double asterisks
  41.             .replace(/\*(.*?)\*/g, '<span style="font-style: italic;">*$1*</span>') // Italics, keep asterisks
  42.             .replace(/^### (.*)/gm, '<span style="color: #6c9c4b;">### $1</span>') // H3 markdown (light salmon)
  43.             .replace(/^## (.*)/gm, '<span style="color: #76c240;">## $1</span>'); // H2 markdown (gold)
  44.         return styledText;
  45.     }
  46.  
  47.     /**
  48.      * Finds the closest scrollable ancestor of an element.
  49.      * Overlays will be positioned relative to #sheld, so scrollable parents *within* #sheld are relevant.
  50.      * @param {HTMLElement} element - The starting element.
  51.      * @returns {HTMLElement[]} An array of scrollable ancestors.
  52.      */
  53.     function findScrollableParents(element) {
  54.         const scrollableParents = [];
  55.         let parent = element.parentElement;
  56.         const sheldElement = document.getElementById(SHELD_ID);
  57.  
  58.         while (parent) {
  59.             // Stop at #sheld, as overlays are now children of #sheld
  60.             if (parent === sheldElement) {
  61.                 // If #sheld itself is scrollable, include it
  62.                 const sheldStyle = window.getComputedStyle(sheldElement);
  63.                 if (sheldStyle.overflowY === 'auto' || sheldStyle.overflowY === 'scroll' ||
  64.                     sheldStyle.overflowX === 'auto' || sheldStyle.overflowX === 'scroll' ||
  65.                     sheldStyle.overflow === 'auto' || sheldStyle.overflow === 'scroll') {
  66.                     scrollableParents.push(sheldElement);
  67.                 }
  68.                 break; // Stop climbing the DOM tree once we reach #sheld
  69.             }
  70.  
  71.             const style = window.getComputedStyle(parent);
  72.             if (style.overflowY === 'auto' || style.overflowY === 'scroll' ||
  73.                 style.overflowX === 'auto' || style.overflowX === 'scroll' ||
  74.                 style.overflow === 'auto' || style.overflow === 'scroll') {
  75.                 scrollableParents.push(parent);
  76.             }
  77.             parent = parent.parentElement;
  78.         }
  79.         return scrollableParents;
  80.     }
  81.  
  82.     /**
  83.      * Updates the position and content of a single overlay.
  84.      * Position is now relative to #sheld.
  85.      * @param {HTMLTextAreaElement} textareaElement - The original textarea element.
  86.      */
  87.     function updateSingleOverlay(textareaElement) {
  88.         const overlayData = managedTextareas.get(textareaElement);
  89.         if (!textareaElement || !overlayData || !overlayData.overlayDiv) {
  90.             return;
  91.         }
  92.  
  93.         const { overlayDiv } = overlayData;
  94.         const rect = textareaElement.getBoundingClientRect(); // Rect relative to viewport
  95.         const computedStyle = window.getComputedStyle(textareaElement);
  96.  
  97.         const sheldElement = document.getElementById(SHELD_ID);
  98.         if (!sheldElement) {
  99.             console.error(`SillyTavern Overlay: #${SHELD_ID} not found during update. Cannot position overlay.`);
  100.             return;
  101.         }
  102.         const sheldRect = sheldElement.getBoundingClientRect(); // Rect of #sheld relative to viewport
  103.  
  104.         // Check if textarea is actually visible and has dimensions
  105.         if (rect.width === 0 || rect.height === 0 || computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
  106.             overlayDiv.style.display = 'none'; // Hide overlay if textarea is hidden
  107.             return;
  108.         } else {
  109.             overlayDiv.style.display = 'block'; // Ensure overlay is visible if textarea is
  110.         }
  111.  
  112.         // Hide the original textarea's text visually but keep it editable
  113.         textareaElement.style.setProperty('color', 'transparent', 'important');
  114.         textareaElement.style.setProperty('caret-color', 'white', 'important');
  115.  
  116.         // Apply position and size relative to #sheld's content area
  117.         // (rect.top - sheldRect.top) calculates position of textarea relative to sheld's top-left corner
  118.         // We no longer subtract textareaElement.scrollTop/scrollLeft here because the overlay itself will scroll
  119.         overlayDiv.style.top = `${rect.top - sheldRect.top + sheldElement.scrollTop}px`;
  120.         overlayDiv.style.left = `${rect.left - sheldRect.left + sheldElement.scrollLeft}px`;
  121.  
  122.         overlayDiv.style.width = `${rect.width}px`;
  123.         overlayDiv.style.height = `${rect.height}px`;
  124.  
  125.         // Match font styles
  126.         overlayDiv.style.fontFamily = computedStyle.fontFamily;
  127.         overlayDiv.style.fontSize = computedStyle.fontSize;
  128.         overlayDiv.style.fontWeight = computedStyle.fontWeight;
  129.         overlayDiv.style.lineHeight = computedStyle.lineHeight;
  130.         overlayDiv.style.padding = computedStyle.padding;
  131.         overlayDiv.style.border = computedStyle.border;
  132.         overlayDiv.style.borderRadius = computedStyle.borderRadius;
  133.  
  134.         // Synchronize the scroll position of the overlay with the textarea
  135.         overlayDiv.scrollTop = textareaElement.scrollTop;
  136.         overlayDiv.scrollLeft = textareaElement.scrollLeft;
  137.  
  138.         // Update content
  139.         overlayDiv.innerHTML = applyStyling(textareaElement.value);
  140.     }
  141.  
  142.     /**
  143.      * Creates and manages an overlay for a given textarea element.
  144.      * The overlay is now appended to #sheld, specifically before #form_sheld.
  145.      * @param {HTMLTextAreaElement} textareaElement - The textarea element to create an overlay for.
  146.      */
  147.     function createAndManageOverlay(textareaElement) {
  148.         if (managedTextareas.has(textareaElement)) {
  149.             return; // Already managing this textarea
  150.         }
  151.  
  152.         const sheldElement = document.getElementById(SHELD_ID);
  153.         if (!sheldElement) {
  154.             console.error(`SillyTavern Overlay: Parent element #${SHELD_ID} not found. Cannot create overlay.`);
  155.             return;
  156.         }
  157.  
  158.         console.log(`SillyTavern Overlay: Creating overlay for new textarea: #${textareaElement.id || Array.from(textareaElement.classList).join('.')}`);
  159.  
  160.         const overlayDiv = document.createElement('div');
  161.         overlayDiv.id = `styled_overlay_${textareaElement.id || Array.from(textareaElement.classList).join('_')}`; // More robust unique ID
  162.         overlayDiv.style.cssText = `
  163.             position: absolute !important; /* Positioned relative to #sheld */
  164.             pointer-events: none !important; /* Allows clicks to pass through to the textarea */
  165.             white-space: pre-wrap !important; /* Preserves whitespace and wraps text */
  166.             word-wrap: break-word !important; /* Breaks long words */
  167.             overflow: auto !important; /* Make the overlay itself scrollable */
  168.             box-sizing: border-box !important; /* Include padding in width/height calculations */
  169.             z-index: 30 !important; /* Set to 30 as per user's request for correct rendering */
  170.             color: #9B9191 !important; /* Default text color */
  171.             background-color: transparent !important; /* Ensure no background to obscure original textarea */
  172.             display: block !important; /* Ensure it's not hidden by default */
  173.             visibility: visible !important; /* Ensure it's visible */
  174.             /* TEMPORARY DEBUGGING STYLES - REMOVE THESE WHEN IT WORKS */
  175.             border: 2px solid cyan !important; /* Bright border to ensure visibility */
  176.             background-color: rgba(0, 0, 0, 0.0) !important; /* Very light background for debugging visibility */
  177.         `;
  178.  
  179.         const formSheldElement = document.getElementById(FORM_SHELD_ID);
  180.  
  181.         // Insert before form_sheld if it exists, otherwise append to sheld
  182.         if (formSheldElement && sheldElement.contains(formSheldElement)) {
  183.             sheldElement.insertBefore(overlayDiv, formSheldElement);
  184.             console.log(`SillyTavern Overlay: Inserted overlay before #${FORM_SHELD_ID}.`);
  185.         } else {
  186.             sheldElement.appendChild(overlayDiv);
  187.             console.log(`SillyTavern Overlay: Appended overlay to #${SHELD_ID} (form_sheld not found or not a child).`);
  188.         }
  189.  
  190.  
  191.         // The non-debounced update function
  192.         const updateFunction = () => updateSingleOverlay(textareaElement);
  193.         // The debounced update function for input/DOM changes (typing, local style changes)
  194.         const debouncedUpdateFunction = debounce(updateFunction, 50);
  195.  
  196.         // Attach event listeners for this specific textarea
  197.         textareaElement.addEventListener('input', debouncedUpdateFunction);
  198.         textareaElement.addEventListener('keyup', debouncedUpdateFunction);
  199.         textareaElement.addEventListener('scroll', debouncedUpdateFunction); // Add listener for textarea's own scroll
  200.  
  201.         // Find and attach scroll listeners to scrollable parent elements *within* #sheld
  202.         const scrollListeners = [];
  203.         findScrollableParents(textareaElement).forEach(scrollParent => {
  204.             const handler = debounce(updateFunction, 20); // Debounce scroll updates slightly
  205.             scrollParent.addEventListener('scroll', handler);
  206.             scrollListeners.push({ element: scrollParent, handler: handler });
  207.             console.log(`SillyTavern Overlay: Attached scroll listener to parent of #${textareaElement.id || Array.from(textareaElement.classList).join('.')}:`, scrollParent);
  208.         });
  209.  
  210.         // Perform initial update with a slight delay to allow rendering
  211.         setTimeout(updateFunction, 100);
  212.  
  213.         // Store references for future management
  214.         managedTextareas.set(textareaElement, {
  215.             overlayDiv: overlayDiv,
  216.             // Create a specific observer for this textarea's local DOM changes
  217.             observer: (() => {
  218.                 const individualObserver = new MutationObserver((mutations) => {
  219.                     let shouldUpdate = false;
  220.                     for (const mutation of mutations) {
  221.                         // Check for changes directly on the textarea or its immediate children
  222.                         if (mutation.target === textareaElement || (textareaElement && textareaElement.contains(mutation.target))) {
  223.                             shouldUpdate = true;
  224.                             break;
  225.                         }
  226.                         // Check for changes on the textarea's parent or its children (for sibling elements affecting layout)
  227.                         if (textareaElement.parentNode && (mutation.target === textareaElement.parentNode || textareaElement.parentNode.contains(mutation.target))) {
  228.                             shouldUpdate = true;
  229.                             break;
  230.                         }
  231.                     }
  232.                     if (shouldUpdate) {
  233.                         debouncedUpdateFunction(); // Use debounced for local changes
  234.                     }
  235.                 });
  236.                 individualObserver.observe(textareaElement, { attributes: true, attributeFilter: ['style', 'class'], childList: true, subtree: true });
  237.                 if (textareaElement.parentNode) {
  238.                     individualObserver.observe(textareaElement.parentNode, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
  239.                 }
  240.                 return individualObserver;
  241.             })(),
  242.             updateFunction: updateFunction, // Non-debounced for global calls
  243.             debouncedUpdateFunction: debouncedUpdateFunction, // Debounced for local events
  244.             scrollListeners: scrollListeners // Array of attached scroll listeners
  245.         });
  246.     }
  247.  
  248.     /**
  249.      * Destroys the overlay and cleans up resources for a given textarea element.
  250.      * @param {HTMLTextAreaElement} textareaElement - The textarea element whose overlay to destroy.
  251.      */
  252.     function destroyOverlay(textareaElement) {
  253.         if (!managedTextareas.has(textareaElement)) {
  254.             return; // Not managing this textarea
  255.         }
  256.  
  257.         console.log(`SillyTavern Overlay: Destroying overlay for removed textarea: #${textareaElement.id || Array.from(textareaElement.classList).join('.')}`);
  258.  
  259.         const { overlayDiv, observer, debouncedUpdateFunction, scrollListeners } = managedTextareas.get(textareaElement);
  260.  
  261.         // Remove event listeners
  262.         textareaElement.removeEventListener('input', debouncedUpdateFunction);
  263.         textareaElement.removeEventListener('keyup', debouncedUpdateFunction);
  264.         textareaElement.removeEventListener('scroll', debouncedUpdateFunction); // Remove listener for textarea's own scroll
  265.  
  266.         // Remove scroll listeners from parent elements
  267.         scrollListeners.forEach(({ element, handler }) => {
  268.             element.removeEventListener('scroll', handler);
  269.         });
  270.  
  271.         // Remove window scroll listener if it's the last overlay
  272.         if (managedTextareas.size === 1) { // If only one left, and we are about to delete it
  273.             window.removeEventListener('scroll', globalUpdateHandler);
  274.         }
  275.  
  276.         // Disconnect its individual MutationObserver
  277.         observer.disconnect();
  278.  
  279.         // Remove the overlay div from the DOM
  280.         if (overlayDiv.parentNode) {
  281.             overlayDiv.parentNode.removeChild(overlayDiv);
  282.         }
  283.  
  284.         // Restore original color of the textarea (optional, but good practice if it's reused)
  285.         textareaElement.style.removeProperty('color');
  286.         textareaElement.style.removeProperty('caret-color');
  287.  
  288.         // Remove from our managed map
  289.         managedTextareas.delete(textareaElement);
  290.     }
  291.  
  292.     /**
  293.      * Main MutationObserver callback to detect added/removed textareas.
  294.      * This observer now focuses only on #sheld's subtree.
  295.      * @param {Array<MutationRecord>} mutations - The array of mutation records.
  296.      */
  297.     function handleDOMMutations(mutations) {
  298.         mutations.forEach(mutation => {
  299.             if (mutation.type === 'childList') {
  300.                 // Handle added nodes
  301.                 mutation.addedNodes.forEach(node => {
  302.                     if (node.nodeType === Node.ELEMENT_NODE) {
  303.                         // Check if the added node itself is a textarea with a target class
  304.                         if (node.tagName === 'TEXTAREA' && Array.from(node.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
  305.                             createAndManageOverlay(node);
  306.                         }
  307.                         // Check if the added node contains any target textareas
  308.                         node.querySelectorAll('textarea').forEach(nestedTextarea => {
  309.                             if (Array.from(nestedTextarea.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
  310.                                 createAndManageOverlay(nestedTextarea);
  311.                             }
  312.                         });
  313.                     }
  314.                 });
  315.  
  316.                 // Handle removed nodes
  317.                 mutation.removedNodes.forEach(node => {
  318.                     if (node.nodeType === Node.ELEMENT_NODE) {
  319.                         // Check if the removed node itself was a managed textarea
  320.                         if (node.tagName === 'TEXTAREA' && managedTextareas.has(node)) {
  321.                             destroyOverlay(node);
  322.                         }
  323.                         // Check if the removed node contained any managed textareas
  324.                         managedTextareas.forEach((_, managedTextarea) => {
  325.                             if (node.contains(managedTextarea)) {
  326.                                 destroyOverlay(managedTextarea);
  327.                             }
  328.                         });
  329.                     }
  330.                 });
  331.             }
  332.         });
  333.     }
  334.  
  335.     // Main observer to watch the #sheld element for textarea additions/removals
  336.     let sheldObserver = null;
  337.  
  338.     // Global listeners for window resize/scroll to update all active overlays
  339.     // This debounced handler calls the *non-debounced* updateFunction for each overlay
  340.     const globalUpdateHandler = debounce(() => {
  341.         managedTextareas.forEach((_, textareaElement) => {
  342.             const overlayData = managedTextareas.get(textareaElement);
  343.             if (overlayData) {
  344.                 overlayData.updateFunction(); // Call the non-debounced update function directly
  345.             }
  346.         });
  347.     }, 10); // Shorter debounce for smoother global response
  348.  
  349.     window.addEventListener('resize', globalUpdateHandler);
  350.     window.addEventListener('scroll', globalUpdateHandler); // Keep global scroll as #sheld itself might scroll into/out of view
  351.  
  352.     // Function to perform initial scan and start main observer
  353.     function initializeDynamicTextareaScript() {
  354.         console.log('SillyTavern Overlay: Initializing dynamic textarea script.');
  355.  
  356.         const sheldElement = document.getElementById(SHELD_ID);
  357.         if (sheldElement) {
  358.             // Start observing #sheld for all changes (childList and subtree)
  359.             sheldObserver = new MutationObserver(handleDOMMutations);
  360.             sheldObserver.observe(sheldElement, { childList: true, subtree: true });
  361.             console.log(`SillyTavern Overlay: Observing #${SHELD_ID} for changes.`);
  362.  
  363.             // Perform an initial scan for existing textareas within #sheld
  364.             sheldElement.querySelectorAll('textarea').forEach(textarea => {
  365.                 if (Array.from(textarea.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
  366.                     createAndManageOverlay(textarea);
  367.                 }
  368.             });
  369.         } else {
  370.             console.warn(`SillyTavern Overlay: #${SHELD_ID} element not found on page load. Retrying.`);
  371.         }
  372.     }
  373.  
  374.     // Start initialization when the DOM is fully loaded
  375.     window.addEventListener('load', initializeDynamicTextareaScript);
  376.  
  377.     // Fallback: If 'load' event doesn't catch it or for SPA-like transitions
  378.     let initCheckInterval = setInterval(() => {
  379.         if (document.body && !sheldObserver && document.getElementById(SHELD_ID)) { // Ensure sheldObserver is not yet active
  380.             clearInterval(initCheckInterval);
  381.             initializeDynamicTextareaScript();
  382.         }
  383.     }, 500);
  384. })();
Advertisement
Add Comment
Please, Sign In to add comment