您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Reverse every selected line (or the whole field) in ANY editable element – even inside shadow-roots. Alt+R or Tampermonkey menu. Debug mode copies the result to the clipboard.
// ==UserScript== // @name Universal Text Reversal // @namespace http://tampermonkey.net/ // @version 1.0 // @license MIT // @description Reverse every selected line (or the whole field) in ANY editable element – even inside shadow-roots. Alt+R or Tampermonkey menu. Debug mode copies the result to the clipboard. // @author AnnaRoblox // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setClipboard // ==/UserScript== (function () { 'use strict'; /* ---------- Debug Mode Configuration ---------- */ // Change this to true to have debug mode enabled by default. let isDebugMode = true; /* ---------- Helpers ---------- */ const RTL_MAGIC = " "; /** * Reverses each line of the provided text and adds a special magic string. * @param {string} text * @returns {string} */ const reverseLines = text => text .split('\n') .map(l => RTL_MAGIC + [...l].reverse().join('')) .join('\n'); /** * Get the real “thing you type into” that is associated with a node. * Walks up the light DOM *and* open shadow DOM trees. * @param {Node} node * @returns {{element:HTMLElement, type:'input'|'textarea'|'contenteditable'|'shadowInput'}|null} */ function locateRealInput(node) { let cur = node; while (cur && cur !== document.documentElement) { if (cur.nodeType !== 1) { cur = cur.parentNode; continue; } // classic form controls if (cur.tagName === 'INPUT' || cur.tagName === 'TEXTAREA') return { element: cur, type: cur.tagName.toLowerCase() }; // simple contenteditable if (cur.contentEditable === 'true') return { element: cur, type: 'contenteditable' }; // dive into open shadow roots (e.g. Monaco, CodeMirror-v6, custom elements) if (cur.shadowRoot) { const sr = cur.shadowRoot; const active = sr.activeElement || sr.querySelector('input, textarea, [contenteditable="true"]'); if (active) return locateRealInput(active); } cur = cur.parentNode || cur.host; // .host jumps out of shadow root } return null; } /** * Copies the given text to the clipboard using the most modern method available. * @param {string} textToCopy */ function copyToClipboard(textToCopy) { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(textToCopy); } else if (navigator.clipboard) { navigator.clipboard.writeText(textToCopy).catch(err => { console.error('Could not copy text to clipboard using modern API: ', err); }); } else { // Fallback for older browsers const textarea = document.createElement('textarea'); textarea.value = textToCopy; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } catch (err) { console.error('Could not copy text to clipboard using execCommand: ', err); } finally { document.body.removeChild(textarea); } } } /* ---------- Core Logic ---------- */ function processSelection() { const sel = window.getSelection(); const inputInfo = locateRealInput(sel.focusNode); if (!inputInfo) { // Nothing editable found – treat as plain text on the page const selected = sel.toString(); if (selected) { const reversed = reverseLines(selected); if (isDebugMode) { copyToClipboard(reversed); } document.execCommand('insertText', false, reversed); } return; } const { element: el, type } = inputInfo; let original, start, end; /* 1. Read original text + caret/selection positions */ if (type === 'input' || type === 'textarea') { original = el.value; start = el.selectionStart; end = el.selectionEnd; } else if (type === 'contenteditable' || type === 'shadowInput') { original = el.textContent || ''; // Map DOM selection to plain-text offsets const range = sel.rangeCount ? sel.getRangeAt(0) : null; if (!range) { start = end = 0; } else { const pre = range.cloneRange(); pre.selectNodeContents(el); pre.setEnd(range.startContainer, range.startOffset); start = pre.toString().length; end = start + range.toString().length; } } /* 2. Reverse the chosen chunk */ const chunk = (start === end) ? original : original.slice(start, end); const reversed = reverseLines(chunk); const replacement = start === end ? reversed : original.slice(0, start) + reversed + original.slice(end); /* 3. If debug mode is on, copy the reversed text to clipboard */ if (isDebugMode) { copyToClipboard(reversed); } /* 4. Write back to the element */ if (type === 'input' || type === 'textarea') { el.value = replacement; el.setSelectionRange(start, start + reversed.length); } else { // contenteditable or shadow el.textContent = replacement; // restore selection const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); let node, offset = 0, startNode, endNode; while (node = walker.nextNode()) { const len = node.textContent.length; if (!startNode && offset + len >= start) { startNode = node; const range = new Range(); range.setStart(startNode, start - offset); range.setEnd(startNode, start - offset + reversed.length); sel.removeAllRanges(); sel.addRange(range); break; } offset += len; } } } /* ---------- Keyboard & Tampermonkey Menu ---------- */ // Keyboard shortcut (Alt+R) document.addEventListener('keydown', e => { if (e.altKey && e.key.toLowerCase() === 'r') { e.preventDefault(); processSelection(); } }); // Register Tampermonkey menu command for the main function GM_registerMenuCommand('Reverse selected lines', processSelection); // Function to toggle debug mode and update the menu command function toggleDebugMode() { isDebugMode = !isDebugMode; updateMenuCommands(); } // Register Tampermonkey menu command for debug mode function updateMenuCommands() { // Clear existing debug menu commands GM_registerMenuCommand('DEBUG: ' + (isDebugMode ? 'ON' : 'OFF'), toggleDebugMode); } // Initial registration of the menu command updateMenuCommands(); })();