您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
优先接管编辑器粘贴管线(UEditor/CKEditor/TinyMCE/Quill),将内容转为纯文本或去样式后放行;否则在捕获阶段统一处理 paste 并原生注入,保留撤销;解锁选中/右键/拖拽;支持同源 iframe 与动态渲染。
// ==UserScript== // @name 完全解除任意网站复制粘贴限制 & 原生复制粘贴使用体验 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 优先接管编辑器粘贴管线(UEditor/CKEditor/TinyMCE/Quill),将内容转为纯文本或去样式后放行;否则在捕获阶段统一处理 paste 并原生注入,保留撤销;解锁选中/右键/拖拽;支持同源 iframe 与动态渲染。 // @author AMT // @match *://*/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; /*** 配置 ***/ const PURE_TEXT = true; // true: 彻底纯文本;false: 仅去 style/class/id/on* 等内联样式与类名,保留基本标签 const ENABLE_GLOBAL_PASTE_CAPTURE = true; // 在捕获阶段统一接管 paste(对大多数站点更稳) /************** 0. 解锁常规禁止:选中 / 右键 / 拖拽 / 长按 **************/ const blocked = ['selectstart','mousedown','mouseup','mousemove','contextmenu','dragstart']; blocked.forEach(ev => addEvent(document, ev, e => e.stopImmediatePropagation(), { capture: true })); onReady(() => { if (document.body) document.body.onselectstart = null; }); const css = document.createElement('style'); css.textContent = `*{user-select:text!important;-webkit-user-select:text!important;}`; document.documentElement.appendChild(css); /************** 1. 优先:编辑器感知式粘贴钩子 **************/ // —— UEditor function hookUEditor(win) { try { const UE = win.UE; if (!UE || !UE.getEditor) return false; const textareas = Array.from(win.document.getElementsByTagName('textarea')) .filter(t => t.id && /answer|ueditor|editor/i.test(t.id)); textareas.forEach(t => { const ed = UE.getEditor(t.id); if (!ed) return; ed.ready(() => { // 尝试卸掉站点自带 beforepaste(若有命名的全局函数) try { ed.removeListener('beforepaste', win.editorPaste); } catch {} // UEditor 的纯文本选项(若支持) try { ed.setOpt && ed.setOpt('pasteplain', true); } catch {} ed.addListener('beforepaste', function (_type, html) { try { if (PURE_TEXT && html && typeof html.text === 'string') { html.html = escapeHtml(html.text); // 彻底纯文本 } else if (html && typeof html.html === 'string') { html.html = sanitizeHTML(html.html); // 去样式/类名 } } catch {} return true; // 放行 }); }); }); return textareas.length > 0; } catch { return false; } } // —— CKEditor 4 function hookCKEditor4(win) { try { const CK = win.CKEDITOR; if (!CK || !CK.instances) return false; let hooked = 0; Object.values(CK.instances).forEach(inst => { if (inst.__amt_paste_hooked) return; inst.on('paste', evt => { const data = evt.data; try { const text = (data && (data.clipboardData?.getData('text/plain') || data.dataValue)) || ''; if (PURE_TEXT) { data.type = 'text'; data.dataValue = escapeHtml(text); } else { data.dataValue = sanitizeHTML(data.dataValue || text); } } catch {} }, null, null, 0); inst.__amt_paste_hooked = true; hooked++; }); return hooked > 0; } catch { return false; } } // —— TinyMCE function hookTinyMCE(win) { try { const TM = win.tinymce; if (!TM || !TM.editors) return false; let hooked = 0; TM.editors.forEach(ed => { if (!ed || ed.destroyed || ed.__amt_paste_hooked) return; ed.on('Paste', e => { try { const dt = e.clipboardData; let text = ''; if (dt) text = dt.getData('text/plain') || dt.getData('text') || ''; if (PURE_TEXT) { e.preventDefault(); ed.insertContent(escapeHtml(text)); } else if (text) { e.preventDefault(); ed.insertContent(sanitizeHTML(text)); } } catch {} }); try { ed.on('keydown', ev => ev.stopImmediatePropagation(), true); } catch {} ed.__amt_paste_hooked = true; hooked++; }); return hooked > 0; } catch { return false; } } // —— Quill function hookQuill(win) { try { const Quill = win.Quill; if (!Quill || !Quill.prototype) return false; // 尝试从全局变量或节点上找到 quill 实例 const nodes = win.document.querySelectorAll('.ql-editor'); let hooked = 0; nodes.forEach(n => { const q = n.__quill || n.parentElement?.__quill || null; if (!q || q.__amt_paste_hooked) return; const Delta = Quill.import && Quill.import('delta'); if (Delta) { q.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => { if (PURE_TEXT) { const text = node.innerText || node.textContent || ''; return new Delta().insert(text); } else { // 去样式:保留文本与基础换行 const text = (node.innerText || node.textContent || '').replace(/\r/g,''); return new Delta().insert(text); } }); q.__amt_paste_hooked = true; hooked++; } }); return hooked > 0; } catch { return false; } } // —— 尝试在主文档与同源 iframe 内多轮挂钩 function tryHookEditors(win) { let any = false; any = hookUEditor(win) || any; any = hookCKEditor4(win) || any; any = hookTinyMCE(win) || any; any = hookQuill(win) || any; return any; } // 初次与轮询/观察(应对异步加载) onReady(() => { // 主文档 bootHook(window, document); // 同源 iframe observeIframes(doc => bootHook(doc.defaultView || window, doc)); }); function bootHook(win, doc) { let tries = 0; const tick = () => { tryHookEditors(win); if (++tries < 60) setTimeout(tick, 300); // 最多尝试 ~18s }; tick(); // DOM 变化也再试(题目区/编辑器异步渲染) const mo = new MutationObserver(() => tryHookEditors(win)); mo.observe(doc.documentElement, { childList: true, subtree: true }); } /************** 2. 通用:捕获阶段统一处理 paste 并原生注入 **************/ if (ENABLE_GLOBAL_PASTE_CAPTURE) { const onPasteCapture = (e) => { const t = findEditableTarget(e); if (!t) return; // 非编辑目标,不处理 // 从剪贴板取纯文本 const dt = e.clipboardData; let text = ''; if (dt) text = dt.getData('text/plain') || dt.getData('text') || ''; // 无文本则放行(比如图片/文件粘贴) if (!text) return; e.stopImmediatePropagation(); e.preventDefault(); const final = PURE_TEXT ? text : stripHTMLToCleanTextOrBasic(text); insertTextAtCursor(t, final); }; addEvent(document, 'paste', onPasteCapture, { capture: true, passive: false }); observeIframes(doc => addEvent(doc, 'paste', onPasteCapture, { capture: true, passive: false })); } /************** 3. 注入实现:保留撤销栈 **************/ function insertTextAtCursor(el, text) { if (!el) return; const doc = el.ownerDocument || document; // 优先用原生命令(很多富文本/框架能把它纳入撤销栈) try { el.focus(); if (doc.execCommand && doc.execCommand('insertText', false, text)) { dispatchInput(el, text); return; } } catch { /* fallthrough */ } // input/textarea if (!el.isContentEditable) { try { const s = el.selectionStart ?? 0, e = el.selectionEnd ?? s; el.setRangeText(text, s, e, 'end'); // 保留撤销 } catch { const val = String(el.value ?? ''); const s = el.selectionStart|0, e = el.selectionEnd|0; setNativeValue(el, val.slice(0, s) + text + val.slice(e)); try { el.setSelectionRange(s + text.length, s + text.length); } catch {} } dispatchInput(el, text); return; } // contenteditable:Range 手动插入 const sel = doc.getSelection && doc.getSelection(); if (sel && sel.rangeCount) { const range = sel.getRangeAt(0); range.deleteContents(); const tn = doc.createTextNode(text); range.insertNode(tn); range.setStartAfter(tn); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); dispatchInput(el, text); } } function dispatchInput(el, data) { // beforeinput try { el.dispatchEvent(new InputEvent('beforeinput', { data, inputType:'insertFromPaste', bubbles:true, cancelable:true })); } catch {} // input try { el.dispatchEvent(new InputEvent('input', { data, inputType:'insertFromPaste', bubbles:true })); } catch { el.dispatchEvent(new Event('input', { bubbles:true })); } // change(站点依赖) try { el.dispatchEvent(new Event('change', { bubbles:true })); } catch {} } /************** 4. 目标判定 / 事件 / iframe / 工具 **************/ function isEditable(el) { if (!el) return false; if (el.isContentEditable) return true; const tag = (el.tagName || '').toLowerCase(); if (tag === 'textarea') return true; if (tag === 'input') { // 避免密码框 return /^(?:text|search|email|url|tel|password|number)$/i.test(el.type || ''); } return false; } function findEditableTarget(e) { // 支持穿过 shadow DOM,沿 composedPath 找最近的可编辑 const path = (e.composedPath && e.composedPath()) || []; for (const n of path) { if (n && isEditable(n)) return n; } // 兜底:activeElement const ae = (e.target && (e.target.ownerDocument || document).activeElement) || document.activeElement; return isEditable(ae) ? ae : null; } function addEvent(target, type, handler, opts) { try { target.addEventListener(type, handler, opts || false); } catch {} } function observeIframes(cb) { const seen = new WeakSet(); const hook = frame => { if (!frame || seen.has(frame)) return; seen.add(frame); try { const doc = frame.contentDocument; if (doc) cb(doc); } catch { /* 跨域 iframe 无法访问 */ } }; const scan = root => root.querySelectorAll('iframe'); onReady(() => scan(document).forEach(hook)); const mo = new MutationObserver(muts => muts.forEach(m => m.addedNodes?.forEach(n => { if (n && n.tagName === 'IFRAME') hook(n); }))); mo.observe(document.documentElement, { childList: true, subtree: true }); } function onReady(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn, { once: true }); } else { fn(); } } function setNativeValue(el, value) { const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const des = Object.getOwnPropertyDescriptor(proto, 'value'); des && des.set && des.set.call(el, value); } /************** 5. 纯文本/去样式处理 **************/ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function sanitizeHTML(html) { // 仅去掉 style/class/id/on* 这类容易被站点拒绝的内联属性;保留标签结构 try { const div = document.createElement('div'); div.innerHTML = html; const walk = node => { if (node.nodeType === 1) { // ELEMENT_NODE // 删除危险/冗余属性 const rm = []; for (const a of node.attributes) { const name = a.name.toLowerCase(); if (name === 'style' || name === 'class' || name === 'id' || name.startsWith('on') || name.startsWith('data-')) { rm.push(a.name); } } rm.forEach(n => node.removeAttribute(n)); } node.childNodes && node.childNodes.forEach(walk); }; div.childNodes.forEach(walk); return div.innerHTML; } catch { // 退一步:去 style/class 的简单正则 return String(html).replace(/\s*(?:style|class|id|on\w+|data-[\w-]+)="[^"]*"/gi, ''); } } function stripHTMLToCleanTextOrBasic(mixed) { // 输入可能是 HTML 或纯文本;先简单判定 if (!/[<>&]/.test(mixed)) return mixed; // 看起来像纯文本 // 尝试 DOM 解析再提取纯文本 try { const div = document.createElement('div'); div.innerHTML = mixed; // 将 <br>/<p> 转为换行 Array.from(div.querySelectorAll('br')).forEach(br => { br.replaceWith('\n'); }); Array.from(div.querySelectorAll('p,div,li')).forEach(el => { if (el.lastChild && el.lastChild.nodeType !== 3) el.appendChild(document.createTextNode('\n')); else el.appendChild(document.createTextNode('\n')); }); return (div.textContent || '').replace(/\u00A0/g, ' '); } catch { // 失败则硬去标签 return String(mixed).replace(/<[^>]+>/g, ''); } } // console.log('[PasteHook] loaded'); })();