Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Styled Dynamic Textarea Overlays - Ultimate True
- // @namespace http://tampermonkey.net/
- // @version 0.5 // Incrementing version for precise insertion before form_sheld
- // @description Overlays styled, non-editable text areas over specific dynamic textareas by class, handles inner scrolls. Inserts overlays before form_sheld.
- // @author Gemini
- // @match *://*/*
- // @grant none
- // ==/UserScript==
- (function() {
- 'use strict';
- // Set of target textarea CLASS NAMES
- const TARGET_TEXTAREA_CLASSES = new Set(['reasoning_edit_textarea', 'edit_textarea']);
- const SHELD_ID = 'sheld'; // The common parent for chat and form_sheld, and the new parent for overlays
- const FORM_SHELD_ID = 'form_sheld'; // The element to insert overlays before
- // Map to store managed textarea elements and their associated overlay data
- // Key: textareaElement, Value: { overlayDiv, observer, updateFunction, debouncedUpdateFunction, scrollListeners: [{element, handler}] }
- const managedTextareas = new Map();
- // Helper for debouncing function calls
- function debounce(func, delay) {
- let timeout;
- return function(...args) {
- const context = this;
- clearTimeout(timeout);
- timeout = setTimeout(() => func.apply(context, args), delay);
- };
- }
- // Function to apply styling based on markdown-like syntax
- function applyStyling(text) {
- let styledText = text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"(.*?)"/g, '<span style="color: #e0a331;">"$1"</span>') // Dialogue text (light blue)
- .replace(/\*\*(.*?)\*\*/g, '<span style="font-weight: bold;">**$1**</span>') // Bold, keep double asterisks
- .replace(/\*(.*?)\*/g, '<span style="font-style: italic;">*$1*</span>') // Italics, keep asterisks
- .replace(/^### (.*)/gm, '<span style="color: #6c9c4b;">### $1</span>') // H3 markdown (light salmon)
- .replace(/^## (.*)/gm, '<span style="color: #76c240;">## $1</span>'); // H2 markdown (gold)
- return styledText;
- }
- /**
- * Finds the closest scrollable ancestor of an element.
- * Overlays will be positioned relative to #sheld, so scrollable parents *within* #sheld are relevant.
- * @param {HTMLElement} element - The starting element.
- * @returns {HTMLElement[]} An array of scrollable ancestors.
- */
- function findScrollableParents(element) {
- const scrollableParents = [];
- let parent = element.parentElement;
- const sheldElement = document.getElementById(SHELD_ID);
- while (parent) {
- // Stop at #sheld, as overlays are now children of #sheld
- if (parent === sheldElement) {
- // If #sheld itself is scrollable, include it
- const sheldStyle = window.getComputedStyle(sheldElement);
- if (sheldStyle.overflowY === 'auto' || sheldStyle.overflowY === 'scroll' ||
- sheldStyle.overflowX === 'auto' || sheldStyle.overflowX === 'scroll' ||
- sheldStyle.overflow === 'auto' || sheldStyle.overflow === 'scroll') {
- scrollableParents.push(sheldElement);
- }
- break; // Stop climbing the DOM tree once we reach #sheld
- }
- const style = window.getComputedStyle(parent);
- if (style.overflowY === 'auto' || style.overflowY === 'scroll' ||
- style.overflowX === 'auto' || style.overflowX === 'scroll' ||
- style.overflow === 'auto' || style.overflow === 'scroll') {
- scrollableParents.push(parent);
- }
- parent = parent.parentElement;
- }
- return scrollableParents;
- }
- /**
- * Updates the position and content of a single overlay.
- * Position is now relative to #sheld.
- * @param {HTMLTextAreaElement} textareaElement - The original textarea element.
- */
- function updateSingleOverlay(textareaElement) {
- const overlayData = managedTextareas.get(textareaElement);
- if (!textareaElement || !overlayData || !overlayData.overlayDiv) {
- return;
- }
- const { overlayDiv } = overlayData;
- const rect = textareaElement.getBoundingClientRect(); // Rect relative to viewport
- const computedStyle = window.getComputedStyle(textareaElement);
- const sheldElement = document.getElementById(SHELD_ID);
- if (!sheldElement) {
- console.error(`SillyTavern Overlay: #${SHELD_ID} not found during update. Cannot position overlay.`);
- return;
- }
- const sheldRect = sheldElement.getBoundingClientRect(); // Rect of #sheld relative to viewport
- // Check if textarea is actually visible and has dimensions
- if (rect.width === 0 || rect.height === 0 || computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
- overlayDiv.style.display = 'none'; // Hide overlay if textarea is hidden
- return;
- } else {
- overlayDiv.style.display = 'block'; // Ensure overlay is visible if textarea is
- }
- // Hide the original textarea's text visually but keep it editable
- textareaElement.style.setProperty('color', 'transparent', 'important');
- textareaElement.style.setProperty('caret-color', 'white', 'important');
- // Apply position and size relative to #sheld's content area
- // (rect.top - sheldRect.top) calculates position of textarea relative to sheld's top-left corner
- // We no longer subtract textareaElement.scrollTop/scrollLeft here because the overlay itself will scroll
- overlayDiv.style.top = `${rect.top - sheldRect.top + sheldElement.scrollTop}px`;
- overlayDiv.style.left = `${rect.left - sheldRect.left + sheldElement.scrollLeft}px`;
- overlayDiv.style.width = `${rect.width}px`;
- overlayDiv.style.height = `${rect.height}px`;
- // Match font styles
- overlayDiv.style.fontFamily = computedStyle.fontFamily;
- overlayDiv.style.fontSize = computedStyle.fontSize;
- overlayDiv.style.fontWeight = computedStyle.fontWeight;
- overlayDiv.style.lineHeight = computedStyle.lineHeight;
- overlayDiv.style.padding = computedStyle.padding;
- overlayDiv.style.border = computedStyle.border;
- overlayDiv.style.borderRadius = computedStyle.borderRadius;
- // Synchronize the scroll position of the overlay with the textarea
- overlayDiv.scrollTop = textareaElement.scrollTop;
- overlayDiv.scrollLeft = textareaElement.scrollLeft;
- // Update content
- overlayDiv.innerHTML = applyStyling(textareaElement.value);
- }
- /**
- * Creates and manages an overlay for a given textarea element.
- * The overlay is now appended to #sheld, specifically before #form_sheld.
- * @param {HTMLTextAreaElement} textareaElement - The textarea element to create an overlay for.
- */
- function createAndManageOverlay(textareaElement) {
- if (managedTextareas.has(textareaElement)) {
- return; // Already managing this textarea
- }
- const sheldElement = document.getElementById(SHELD_ID);
- if (!sheldElement) {
- console.error(`SillyTavern Overlay: Parent element #${SHELD_ID} not found. Cannot create overlay.`);
- return;
- }
- console.log(`SillyTavern Overlay: Creating overlay for new textarea: #${textareaElement.id || Array.from(textareaElement.classList).join('.')}`);
- const overlayDiv = document.createElement('div');
- overlayDiv.id = `styled_overlay_${textareaElement.id || Array.from(textareaElement.classList).join('_')}`; // More robust unique ID
- overlayDiv.style.cssText = `
- position: absolute !important; /* Positioned relative to #sheld */
- pointer-events: none !important; /* Allows clicks to pass through to the textarea */
- white-space: pre-wrap !important; /* Preserves whitespace and wraps text */
- word-wrap: break-word !important; /* Breaks long words */
- overflow: auto !important; /* Make the overlay itself scrollable */
- box-sizing: border-box !important; /* Include padding in width/height calculations */
- z-index: 30 !important; /* Set to 30 as per user's request for correct rendering */
- color: #9B9191 !important; /* Default text color */
- background-color: transparent !important; /* Ensure no background to obscure original textarea */
- display: block !important; /* Ensure it's not hidden by default */
- visibility: visible !important; /* Ensure it's visible */
- /* TEMPORARY DEBUGGING STYLES - REMOVE THESE WHEN IT WORKS */
- border: 2px solid cyan !important; /* Bright border to ensure visibility */
- background-color: rgba(0, 0, 0, 0.0) !important; /* Very light background for debugging visibility */
- `;
- const formSheldElement = document.getElementById(FORM_SHELD_ID);
- // Insert before form_sheld if it exists, otherwise append to sheld
- if (formSheldElement && sheldElement.contains(formSheldElement)) {
- sheldElement.insertBefore(overlayDiv, formSheldElement);
- console.log(`SillyTavern Overlay: Inserted overlay before #${FORM_SHELD_ID}.`);
- } else {
- sheldElement.appendChild(overlayDiv);
- console.log(`SillyTavern Overlay: Appended overlay to #${SHELD_ID} (form_sheld not found or not a child).`);
- }
- // The non-debounced update function
- const updateFunction = () => updateSingleOverlay(textareaElement);
- // The debounced update function for input/DOM changes (typing, local style changes)
- const debouncedUpdateFunction = debounce(updateFunction, 50);
- // Attach event listeners for this specific textarea
- textareaElement.addEventListener('input', debouncedUpdateFunction);
- textareaElement.addEventListener('keyup', debouncedUpdateFunction);
- textareaElement.addEventListener('scroll', debouncedUpdateFunction); // Add listener for textarea's own scroll
- // Find and attach scroll listeners to scrollable parent elements *within* #sheld
- const scrollListeners = [];
- findScrollableParents(textareaElement).forEach(scrollParent => {
- const handler = debounce(updateFunction, 20); // Debounce scroll updates slightly
- scrollParent.addEventListener('scroll', handler);
- scrollListeners.push({ element: scrollParent, handler: handler });
- console.log(`SillyTavern Overlay: Attached scroll listener to parent of #${textareaElement.id || Array.from(textareaElement.classList).join('.')}:`, scrollParent);
- });
- // Perform initial update with a slight delay to allow rendering
- setTimeout(updateFunction, 100);
- // Store references for future management
- managedTextareas.set(textareaElement, {
- overlayDiv: overlayDiv,
- // Create a specific observer for this textarea's local DOM changes
- observer: (() => {
- const individualObserver = new MutationObserver((mutations) => {
- let shouldUpdate = false;
- for (const mutation of mutations) {
- // Check for changes directly on the textarea or its immediate children
- if (mutation.target === textareaElement || (textareaElement && textareaElement.contains(mutation.target))) {
- shouldUpdate = true;
- break;
- }
- // Check for changes on the textarea's parent or its children (for sibling elements affecting layout)
- if (textareaElement.parentNode && (mutation.target === textareaElement.parentNode || textareaElement.parentNode.contains(mutation.target))) {
- shouldUpdate = true;
- break;
- }
- }
- if (shouldUpdate) {
- debouncedUpdateFunction(); // Use debounced for local changes
- }
- });
- individualObserver.observe(textareaElement, { attributes: true, attributeFilter: ['style', 'class'], childList: true, subtree: true });
- if (textareaElement.parentNode) {
- individualObserver.observe(textareaElement.parentNode, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
- }
- return individualObserver;
- })(),
- updateFunction: updateFunction, // Non-debounced for global calls
- debouncedUpdateFunction: debouncedUpdateFunction, // Debounced for local events
- scrollListeners: scrollListeners // Array of attached scroll listeners
- });
- }
- /**
- * Destroys the overlay and cleans up resources for a given textarea element.
- * @param {HTMLTextAreaElement} textareaElement - The textarea element whose overlay to destroy.
- */
- function destroyOverlay(textareaElement) {
- if (!managedTextareas.has(textareaElement)) {
- return; // Not managing this textarea
- }
- console.log(`SillyTavern Overlay: Destroying overlay for removed textarea: #${textareaElement.id || Array.from(textareaElement.classList).join('.')}`);
- const { overlayDiv, observer, debouncedUpdateFunction, scrollListeners } = managedTextareas.get(textareaElement);
- // Remove event listeners
- textareaElement.removeEventListener('input', debouncedUpdateFunction);
- textareaElement.removeEventListener('keyup', debouncedUpdateFunction);
- textareaElement.removeEventListener('scroll', debouncedUpdateFunction); // Remove listener for textarea's own scroll
- // Remove scroll listeners from parent elements
- scrollListeners.forEach(({ element, handler }) => {
- element.removeEventListener('scroll', handler);
- });
- // Remove window scroll listener if it's the last overlay
- if (managedTextareas.size === 1) { // If only one left, and we are about to delete it
- window.removeEventListener('scroll', globalUpdateHandler);
- }
- // Disconnect its individual MutationObserver
- observer.disconnect();
- // Remove the overlay div from the DOM
- if (overlayDiv.parentNode) {
- overlayDiv.parentNode.removeChild(overlayDiv);
- }
- // Restore original color of the textarea (optional, but good practice if it's reused)
- textareaElement.style.removeProperty('color');
- textareaElement.style.removeProperty('caret-color');
- // Remove from our managed map
- managedTextareas.delete(textareaElement);
- }
- /**
- * Main MutationObserver callback to detect added/removed textareas.
- * This observer now focuses only on #sheld's subtree.
- * @param {Array<MutationRecord>} mutations - The array of mutation records.
- */
- function handleDOMMutations(mutations) {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- // Handle added nodes
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Check if the added node itself is a textarea with a target class
- if (node.tagName === 'TEXTAREA' && Array.from(node.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
- createAndManageOverlay(node);
- }
- // Check if the added node contains any target textareas
- node.querySelectorAll('textarea').forEach(nestedTextarea => {
- if (Array.from(nestedTextarea.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
- createAndManageOverlay(nestedTextarea);
- }
- });
- }
- });
- // Handle removed nodes
- mutation.removedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Check if the removed node itself was a managed textarea
- if (node.tagName === 'TEXTAREA' && managedTextareas.has(node)) {
- destroyOverlay(node);
- }
- // Check if the removed node contained any managed textareas
- managedTextareas.forEach((_, managedTextarea) => {
- if (node.contains(managedTextarea)) {
- destroyOverlay(managedTextarea);
- }
- });
- }
- });
- }
- });
- }
- // Main observer to watch the #sheld element for textarea additions/removals
- let sheldObserver = null;
- // Global listeners for window resize/scroll to update all active overlays
- // This debounced handler calls the *non-debounced* updateFunction for each overlay
- const globalUpdateHandler = debounce(() => {
- managedTextareas.forEach((_, textareaElement) => {
- const overlayData = managedTextareas.get(textareaElement);
- if (overlayData) {
- overlayData.updateFunction(); // Call the non-debounced update function directly
- }
- });
- }, 10); // Shorter debounce for smoother global response
- window.addEventListener('resize', globalUpdateHandler);
- window.addEventListener('scroll', globalUpdateHandler); // Keep global scroll as #sheld itself might scroll into/out of view
- // Function to perform initial scan and start main observer
- function initializeDynamicTextareaScript() {
- console.log('SillyTavern Overlay: Initializing dynamic textarea script.');
- const sheldElement = document.getElementById(SHELD_ID);
- if (sheldElement) {
- // Start observing #sheld for all changes (childList and subtree)
- sheldObserver = new MutationObserver(handleDOMMutations);
- sheldObserver.observe(sheldElement, { childList: true, subtree: true });
- console.log(`SillyTavern Overlay: Observing #${SHELD_ID} for changes.`);
- // Perform an initial scan for existing textareas within #sheld
- sheldElement.querySelectorAll('textarea').forEach(textarea => {
- if (Array.from(textarea.classList).some(cls => TARGET_TEXTAREA_CLASSES.has(cls))) {
- createAndManageOverlay(textarea);
- }
- });
- } else {
- console.warn(`SillyTavern Overlay: #${SHELD_ID} element not found on page load. Retrying.`);
- }
- }
- // Start initialization when the DOM is fully loaded
- window.addEventListener('load', initializeDynamicTextareaScript);
- // Fallback: If 'load' event doesn't catch it or for SPA-like transitions
- let initCheckInterval = setInterval(() => {
- if (document.body && !sheldObserver && document.getElementById(SHELD_ID)) { // Ensure sheldObserver is not yet active
- clearInterval(initCheckInterval);
- initializeDynamicTextareaScript();
- }
- }, 500);
- })();
Advertisement
Add Comment
Please, Sign In to add comment