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

完全解除复制/粘贴限制,保留 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 });
  }
})();