Greasy Fork is available in English.
完全解除复制/粘贴限制,保留 Ctrl+Z 撤销,并兼容大部分浏览器环境
当前为
// ==UserScript==
// @name 完全解除任意网站复制粘贴限制 & 原生复制粘贴使用体验
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 完全解除复制/粘贴限制,保留 Ctrl+Z 撤销,并兼容大部分浏览器环境
// @author AMT
// @match *://*/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/************** 0. 解除常规禁用选中 / 右键 / 拖拽 / 长按 **************/
const blocked = [
'selectstart', 'mousedown', 'mouseup', 'mousemove',
'contextmenu', 'dragstart'
];
blocked.forEach(ev =>
document.addEventListener(ev, e => e.stopImmediatePropagation(), true)
);
if (document.body) document.body.onselectstart = null;
// 强制允许选中
const css = document.createElement('style');
css.textContent = `
*{user-select:text!important;-webkit-user-select:text!important;}
`;
document.head.appendChild(css);
/************** 1. 统一处理 Ctrl/⌘+V **************/
const isMac = /Mac/i.test(navigator.platform);
/** 监听主文档的粘贴快捷键 */
document.addEventListener('keydown', onKeyPaste, true);
/** 监听所有同源 iframe(学习通很多编辑器在 iframe 内) */
observeIframes(node => node.addEventListener('keydown', onKeyPaste, true));
async function onKeyPaste(e) {
const isPasteKey = (isMac ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === 'v';
if (!isPasteKey) return;
const target = document.activeElement;
if (!isEditable(target)) return;
e.stopImmediatePropagation();
e.preventDefault();
// 读剪贴板
let text = await readClipboard();
if (!text) return;
// 1) 先尝试最标准的 insertText —— 成功率最高,支持撤销
if (tryExecCommand(text, target)) return;
/* -----------------------------------------------------------
2) fallback:逐段“模拟键盘输入”,绕过 paste 封锁
--------------------------------------------------------- */
simulateTyping(text, target);
}
/************** 2. 辅助函数 **************/
/** 判断可编辑元素(input / textarea / contenteditable) */
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)$/i.test(el.type || '');
}
return false;
}
/** 读取剪贴板文字(多浏览器兜底) */
async function readClipboard() {
// 新 API
if (navigator.clipboard?.readText) {
try { const t = await navigator.clipboard.readText(); if (t) return t; } catch { }
}
// 旧 API
const legacy = (document.clipboardData || window.clipboardData);
return legacy ? legacy.getData('text') : '';
}
/** execCommand 插入文本,成功返回 true */
function tryExecCommand(text, el) {
try {
el.focus();
/* 对部分富文本框,只有先选中光标位置再 execCommand 才能奏效 */
if (document.execCommand('insertText', false, text)) {
dispatchInput(el, text);
return true;
}
} catch { /* fall through */ }
return false;
}
/** fallback:把文本像键盘一样“敲”进去(一次性插入,效率 > 单字符) */
function simulateTyping(text, el) {
if (!el) return;
const isInput = !el.isContentEditable;
/* 保存光标 & 原内容 */
let start = 0, end = 0, original = '';
if (isInput) {
start = el.selectionStart;
end = el.selectionEnd;
original = el.value;
} else {
/* contentEditable 统计纯文本光标偏移——简单场景足够 */
const sel = el.ownerDocument.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
start = range.startOffset;
end = range.endOffset;
original = el.textContent;
}
/* 计算插入后内容 */
const newValue = original.slice(0, start) + text + original.slice(end);
/* 写入内容(对 React/Vue 等框架,用原生 setter 触发监听) */
if (isInput) {
const setter = Object.getOwnPropertyDescriptor(
el.tagName === 'INPUT' ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype,
'value'
).set;
setter.call(el, newValue);
} else {
el.textContent = newValue;
}
/* 恢复光标到粘贴尾部 */
if (isInput && el.setSelectionRange) {
el.setSelectionRange(start + text.length, start + text.length);
} else if (el.isContentEditable) {
const sel = el.ownerDocument.getSelection();
sel.removeAllRanges();
const range = el.ownerDocument.createRange();
range.setStart(el.firstChild || el, start + text.length);
range.collapse(true);
sel.addRange(range);
}
dispatchInput(el, text);
}
/** 统一派发 beforeinput/input/change,保证框架同步 v-model/双向绑定 */
function dispatchInput(el, data) {
/* beforeinput(某些编辑器会用) */
el.dispatchEvent(new InputEvent('beforeinput', {
data, inputType: 'insertFromPaste', bubbles: true, cancelable: true
}));
/* input */
el.dispatchEvent(new InputEvent('input', {
data, inputType: 'insertFromPaste', bubbles: true
}));
/* change(有需要就派,不影响正常撤销) */
el.dispatchEvent(new Event('change', { bubbles: true }));
}
/** 监听/递归注入同源 iframe(学习通作业答题区常见) */
function observeIframes(cb) {
const handled = new WeakSet();
const tryHook = frame => {
if (handled.has(frame)) return;
handled.add(frame);
try { cb(frame.contentDocument); } catch { /* 跨域 iframe 无权限 */ }
};
/* 当前已存在的 iframe */
document.querySelectorAll('iframe').forEach(tryHook);
/* 后续动态插入的 iframe */
new MutationObserver(muts => {
muts.forEach(m => {
m.addedNodes.forEach(node => {
if (node.tagName === 'IFRAME') tryHook(node);
});
});
}).observe(document.documentElement, { childList: true, subtree: true });
}
})();