Greasy Fork

Greasy Fork is available in English.

解鎖圖片右鍵與選取(全站版,可 per-domain 關閉)

解除各站 IMG 的 oncontextmenu/onmousedown/onselectstart/ondragstart 等攔截,恢復右鍵、選取、拖曳;支援 per-domain 開關與 file://。

当前为 2025-10-12 提交的版本,查看 最新版本

// ==UserScript==
// @name         解鎖圖片右鍵與選取(全站版,可 per-domain 關閉)
// @namespace    http://greasyfork.icu/users/your-id
// @version      1.1.0
// @description  解除各站 IMG 的 oncontextmenu/onmousedown/onselectstart/ondragstart 等攔截,恢復右鍵、選取、拖曳;支援 per-domain 開關與 file://。
// @author       you
// @match        *://*/*
// @match        file:///*
// @run-at       document-start
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  const KEY = 'unlock_img_rightclick.disabled_domains';
  const ATTRS = ['oncontextmenu','onmousedown','onselectstart','ondragstart'];
  const domain = location.hostname || 'file';

  const getSet = async () => new Set(JSON.parse(await GM_getValue(KEY, '[]')));
  const saveSet = async (set) => GM_setValue(KEY, JSON.stringify([...set]));

  // Menu: toggle current domain
  (async () => {
    const disabled = await getSet();
    const isDisabled = disabled.has(domain);
    GM_registerMenuCommand(
      `${isDisabled ? '啟用' : '停用'} 此網域:${domain}`,
      async () => {
        if (isDisabled) disabled.delete(domain);
        else disabled.add(domain);
        await saveSet(disabled);
        location.reload();
      }
    );
    GM_registerMenuCommand('查看已停用網域', async () => {
      const list = [...await getSet()].sort().join('\n') || '(無)';
      alert(list);
    });
    GM_registerMenuCommand('清空停用清單', async () => {
      if (confirm('確定清空所有停用網域?')) await saveSet(new Set());
      location.reload();
    });
    if (isDisabled) return; // do nothing on disabled domains
    init();
  })();

  function init() {
    // CSS:恢復拖曳/選取與指針事件
    GM_addStyle(`
      img { -webkit-user-drag: auto !important; user-select: auto !important; }
      img, picture, figure { pointer-events: auto !important; }
    `);

    // 捕獲階段阻擋站方攔截器(不阻止預設行為)
    ['contextmenu','mousedown','selectstart','dragstart'].forEach(type => {
      document.addEventListener(type, ev => {
        if (ev.target instanceof HTMLImageElement) {
          ev.stopImmediatePropagation(); // 擋站方處理器
          // 不呼叫 preventDefault(),保留瀏覽器右鍵
        }
      }, true);
    });

    const cleanNode = (node) => {
      if (!(node instanceof Element)) return;
      if (node instanceof HTMLImageElement || ATTRS.some(a => node.hasAttribute(a))) {
        for (const a of ATTRS) {
          if (node.hasAttribute(a)) node.removeAttribute(a);
          try { node[a] = null; } catch {}
        }
        if (node instanceof HTMLImageElement) {
          node.style.userSelect = 'auto';
          node.style.webkitUserDrag = 'auto';
        }
      }
    };

    const scan = (root) => {
      cleanNode(root);
      root.querySelectorAll?.('img,[oncontextmenu],[onmousedown],[onselectstart],[ondragstart]')
        .forEach(cleanNode);
    };

    const mo = new MutationObserver(muts => {
      for (const m of muts) {
        if (m.type === 'childList') {
          m.addedNodes.forEach(scan);
        } else if (m.type === 'attributes') {
          cleanNode(m.target);
        }
      }
    });
    mo.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ATTRS
    });

    if (document.readyState !== 'loading') scan(document);
    else document.addEventListener('DOMContentLoaded', () => scan(document));
  }
})();