Greasy Fork

Greasy Fork is available in English.

完全解除任意网站复制粘贴限制 & 原生复制粘贴使用体验

完全解除复制/粘贴限制,保留 Ctrl+Z 撤销,并兼容大部分浏览器环境

当前为 2025-07-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 });
  }
})();