【油猴脚本】Edge Web 选择的平替 - 框选复制文本

粗体文本较新版本的 Edge 砍掉了 Web 选择,可在快捷键方式添加启动参数找回
--enable-features=msEdgeAreaSelect

Windows 在这有个小坑: 将带启动参数或便携式浏览器设置为默认浏览器

我用的是 GitHub - SiL3NC3/PortableRegistrator: Easily register any portable app as a default program in Windows XP, 7, 8, 10

但有的浏览器似乎不起作用

或者是手动修改注册表,也试过,有点麻烦,效果也不理想

如果有更好的方法也请指教


平替脚本(非 Edge 浏览器的选择)

1. Web选择

不太习惯这个脚本的触发方式,只试用了一下

2. 框选复制 beta

瞎捣鼓出来的,要求不高的可以试试,简单分享,算不上推荐

代码(点击查看):
// ==UserScript==
// @name 框选复制文本 beta
// @namespace http://tampermonkey.net/
// @version 0.5
// @description  修饰键 + 左键拖拽框选松开后自动复制框选文本
// @author You
// @match *://*/*
// @grant none
// ==/UserScript==

(function () {
  'use strict';

  let selectionBox = null;
  let isSelecting = false;
  let startX, startY;
  let highlightedElements = [];
  let splitMode = false;
  let mask = null;


  const isMac = /Mac/.test(navigator.userAgent);
  const modKey = isMac ? 'metaKey' : 'ctrlKey';

  // [modKey]:Mac 为 metaKey ,Windows 为 ctrlKey
  // 非拆分模式修饰键
  const nonSplitModeShortcut = { alt: true, [ modKey ]: false, shift: false };
  // 拆分模式修饰键
  const splitModeShortcut = { alt: false, [ modKey ]: false, shift: false };
  // 是否屏蔽修饰键 + 左键的默认行为,true 为开启,false 为关闭
  let isMask = false;



  function createSelectionBox(x, y, splitMode) {
    const box = document.createElement('div');
    box.style.border = splitMode ? '3px dotted #62a262' : '3px dashed #62a262';
    box.style.position = 'absolute';
    box.style.zIndex = '2147483647';
    box.style.pointerEvents = 'none';
    box.style.left = `${ x }px`;
    box.style.top = `${ y }px`;
    return box;
  }

  function updateSelectionBox(box, startX, startY, currentX, currentY) {
    const minX = Math.min(startX, currentX);
    const minY = Math.min(startY, currentY);
    const width = Math.abs(startX - currentX);
    const height = Math.abs(startY - currentY);
    box.style.left = `${ minX }px`;
    box.style.top = `${ minY }px`;
    box.style.width = `${ width }px`;
    box.style.height = `${ height }px`;
  }

  function clearHighlightedElements() {
    highlightedElements.forEach(el => {
      el.style.outline = '';
      el.style.backgroundColor = '';
    });
    highlightedElements = [];
  }

  function highlightElement(el) {
    el.style.outline = '2px solid rgb(135, 206, 250)';
    el.style.backgroundColor = 'rgba(135, 206, 250, 0.3)';
    highlightedElements.push(el);
  }

  function elementDirectlyContainsText(el, splitMode) {
    if (!splitMode) {
      return Array.from(el.childNodes).some(node =>
        node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '');
    } else {
      let textNodes = Array.from(el.childNodes).filter(node =>
        node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '');

      textNodes.forEach(textNode => {
        let textSegments = textNode.textContent.split(/<br\s*\/?>|\n/);
        textSegments.forEach(segment => {
          if (!segment.trim()) return;
          let range = document.createRange();
          let startIndex = textNode.textContent.indexOf(segment);
          range.setStart(textNode, startIndex);
          range.setEnd(textNode, startIndex + segment.length);
          let span = document.createElement('span');
          span.style.outline = '2px solid rgb(135, 206, 250)';
          span.style.backgroundColor = 'rgba(135, 206, 250, 0.3)';
          span.setAttribute('data-split-text', 'true');
          range.surroundContents(span);
          highlightedElements.push(span);
        });
      });
      return textNodes.length > 0;
    }
  }

  function isRectOverlap(rect1, rect2) {
    return !(rect1.right < rect2.left || rect1.left > rect2.right ||
      rect1.bottom < rect2.top || rect1.top > rect2.bottom);
  }

  function highlightElementsInRect(rect, splitMode) {
    if (!splitMode) {
      clearHighlightedElements();
      const elements = document.querySelectorAll('*:not(html):not(body):not(script)');
      let possibleElements = [];
      elements.forEach(el => {
        const elRect = el.getBoundingClientRect();
        if (isRectOverlap(rect, elRect) && elementDirectlyContainsText(el, splitMode)) {
          possibleElements.push(el);
        }
      });

      possibleElements.forEach(el => {
        let parent = el.parentElement;
        let isChildOfHighlighted = false;
        while (parent) {
          if (possibleElements.includes(parent)) {
            isChildOfHighlighted = true;
            break;
          }
          parent = parent.parentElement;
        }
        if (!isChildOfHighlighted) {
          highlightElement(el);
        }
      });
    } else {
      clearHighlightedElements();
      let elements = document.querySelectorAll('*:not(html):not(body):not(script)');
      elements.forEach(el => {
        let elRect = el.getBoundingClientRect();
        if (isRectOverlap(rect, elRect)) {
          elementDirectlyContainsTextSplitMode(el);
        }
      });
    }
  }

  function elementDirectlyContainsTextSplitMode(el) {
    const textNodes = Array.from(el.childNodes).filter(node =>
      node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '');

    textNodes.forEach(textNode => {
      const textSegments = textNode.textContent.split(/<br\s*\/?>|\n/);
      textSegments.forEach(segment => {
        if (!segment.trim()) return;
        const range = document.createRange();
        const startIndex = textNode.textContent.indexOf(segment);
        range.setStart(textNode, startIndex);
        range.setEnd(textNode, startIndex + segment.length);

        const wrapper = document.createElement('span');
        wrapper.textContent = segment;
        wrapper.style.outline = '2px solid rgb(135, 206, 250)';
        wrapper.style.backgroundColor = 'rgba(135, 206, 250, 0.3)';
        wrapper.setAttribute('data-split-text', 'true');

        range.deleteContents();
        range.insertNode(wrapper);

        highlightedElements.push(wrapper);
      });
    });

    return textNodes.length > 0;
  }

  function removeElementsWithDuplicatedText(elements) {
    const uniqueTextContents = new Map();

    elements.forEach(el => {
      const textContent = el.innerText.replace(/\s+/g, '');
      if (uniqueTextContents.has(textContent)) {
        uniqueTextContents.get(textContent).push(el);
      } else {
        uniqueTextContents.set(textContent, [ el ]);
      }
    });

    return [].concat(...Array.from(uniqueTextContents.values()));
  }

  function copyTextOfHighlightedElements(splitMode) {
    let textContents;
    if (!splitMode) {
      textContents = highlightedElements.map(el => el.innerText.trim() + '\n').join('');
      textContents = textContents.replace(/^\s*[\r\n]/gm, '');
    } else {
      const uniqueHighlightedElements = removeElementsWithDuplicatedText(highlightedElements);
      textContents = uniqueHighlightedElements.map(el => el.innerText.trim()).join('\n');
      textContents = textContents.replace(/^\s*[\r\n]/gm, '');
    }

    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
      navigator.clipboard.writeText(textContents).then(() => {
        // console.log('复制成功');
      }).catch(err => {
        console.error('复制失败: ', err);
        fallbackCopyTextToClipboard(textContents);
      });
    } else {
      fallbackCopyTextToClipboard(textContents);
    }

    if (splitMode) {
      highlightedElements.forEach(el => {
        if (el.getAttribute('data-split-text') === 'true') {
          el.outerHTML = el.textContent;
        }
      });
      highlightedElements = [];
    }
  }

  function fallbackCopyTextToClipboard(text) {
    const textArea = document.createElement('textarea');
    textArea.value = text;
    textArea.style.position = 'fixed';
    textArea.style.opacity = '0';
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();

    try {
      const successful = document.execCommand('copy');
      const msg = successful ? '复制成功' : '复制失败';
      // console.log(msg);
    } catch (err) {
      console.error('复制失败: ', err);
    }

    document.body.removeChild(textArea);
  }

  function maskMouseUpEvent() {
    mask = document.createElement('div');
    mask.style.position = 'fixed';
    mask.style.top = '0';
    mask.style.left = '0';
    mask.style.width = '100vw';
    mask.style.height = '100vh';
    mask.style.zIndex = '999999';
    mask.style.pointerEvents = 'auto';
    mask.style.background = 'rgba(0,0,0,0)';

    document.body.appendChild(mask);

    mask.addEventListener('mouseup', function (maskMouseUpEvent) {
      if (isSelecting) {
        isSelecting = false;
        const selectionRect = selectionBox.getBoundingClientRect();
        highlightElementsInRect(selectionRect, splitMode);
        copyTextOfHighlightedElements(splitMode);
        clearHighlightedElements();
        document.body.removeChild(selectionBox);
        selectionBox = null;

        document.body.removeChild(mask);
        mask = null;
        maskMouseUpEvent.preventDefault();
        maskMouseUpEvent.stopPropagation();
      }
    });
  }

  document.addEventListener('mousedown', function (e) {
    if ((nonSplitModeShortcut.alt ? e.altKey : !e.altKey) &&
      (nonSplitModeShortcut[ modKey ] ? e[ modKey ] : !e[ modKey ]) &&
      (nonSplitModeShortcut.shift ? e.shiftKey : !e.shiftKey) &&
      e.button === 0 &&
      (nonSplitModeShortcut.alt || nonSplitModeShortcut[ modKey ] || nonSplitModeShortcut.shift)) {
      e.preventDefault();
      isSelecting = true;
      startX = e.pageX;
      startY = e.pageY;
      selectionBox = createSelectionBox(startX, startY, false);
      document.body.appendChild(selectionBox);
      splitMode = false;
      if (isMask) {
        maskMouseUpEvent()
      }
    } else if ((splitModeShortcut.alt ? e.altKey : !e.altKey) &&
      (splitModeShortcut[ modKey ] ? e[ modKey ] : !e[ modKey ]) &&
      (splitModeShortcut.shift ? e.shiftKey : !e.shiftKey) &&
      e.button === 0 &&
      (splitModeShortcut.alt || splitModeShortcut[ modKey ] || splitModeShortcut.shift)) {
      e.preventDefault();
      isSelecting = true;
      startX = e.pageX;
      startY = e.pageY;
      selectionBox = createSelectionBox(startX, startY, true);
      document.body.appendChild(selectionBox);
      splitMode = true;
      if (isMask) {
        maskMouseUpEvent()
      }
    }
  }, false);

  document.addEventListener('mousemove', function (e) {
    if (isSelecting) {
      updateSelectionBox(selectionBox, startX, startY, e.pageX, e.pageY);
      const selectionRect = selectionBox.getBoundingClientRect();
      highlightElementsInRect(selectionRect, splitMode);
    }
  }, false);

  if (!isMask) {
    document.addEventListener('mouseup', function (e) {
      if (isSelecting) {
        isSelecting = false;
        const selectionRect = selectionBox.getBoundingClientRect();
        highlightElementsInRect(selectionRect, splitMode);
        copyTextOfHighlightedElements(splitMode);
        clearHighlightedElements();
        document.body.removeChild(selectionBox);
        selectionBox = null;
      }
    }, false);
  }

  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') {
      clearHighlightedElements();
      if (selectionBox) {
        document.body.removeChild(selectionBox);
        selectionBox = null;
      }
    }
  }, false);


})();

安装:在脚本管理器(篡改猴 Tampermonkey、暴力猴 Violentmonkey 等)面板中,添加新脚本

用法:默认 Alt + 左键 拖拽框选松开后自动复制框选文本(非拆分模式),如果松开左键后遇到选择框没有自动隐藏按 ESC

有两种选择模式(默认只启用非拆分模式,都启用需要自定义修饰键)

  1. 拆分模式:对嵌套换行标签的元素节点进行拆分,效果是换行标签前后文本,可单独复制
  2. 非拆分模式:不拆分嵌套换行标签的元素节点,直接复制整段文本

如果先对含换行标签的元素节点使用拆分模式,会破坏 html 结构(样式基本不变,页面刷新后会复原),再对其使用非拆分模式就无法直接复制整段文本

具体效果试下就懂了

脚本默认的修饰键会破坏原有 Alt + 左键 时在链接上选择文本的功能
可自定义修饰键,需要按住触发的键改为 true,反之 false

// [modKey]:Mac 为 metaKey ,Windows 为 ctrlKey
// 非拆分模式修饰键
const nonSplitModeShortcut = { alt: true, [modKey]: false, shift: false };
// 拆分模式修饰键
const splitModeShortcut = { alt: false, [modKey]: false, shift: false };
// 是否屏蔽修饰键 + 左键的默认行为,true 为开启,false 为关闭
let isMask = false;

有在链接上选择部分文本需求的,可以试下这个扩展使用的, js 源码也能直接转成油猴脚本
GitHub - lcandy2/Select-like-a-Boss: Select link’s text just like a regular text (like in Opera’12 browser) - Select like a Boss :wink:

修饰键 + 左键单击也能快速复制

偶然发现用来摘录部分文本还挺方便的,还有在 ai 对话中没完全回答完成时快速复制(代码段也可以,在代码段的空白处点击或拖拽)

部分限制了复制文本的网站也能直接框选复制

可能有 bug,但不一定能修看情况,HTML 结构比较复杂的页面还是建议直接 OCR

如果有感兴趣的大佬能够完善或者优化这个脚本就更好了

待优化:

  • 原生表格和 div 表格按照原格式复制,考虑到前端框架或组件的表格基本上都能导出数据,感觉意义不大,再者直接复制就能保留原格式

更新

  • v0.2:改为双模式
  • v0.3:双模式选择框样式区分
  • v0.4:添加新的复制API,兼容更多页面
  • v0.5:可在同一链接上点击或拖拽直接复制链接标题,由于此功能会破坏浏览器默认行为,默认是关闭的,需要手动开启

效果图

  • 列复制
    image_6

  • 复制 AI 对话
    image_1


相关讨论:

1 个赞

可以参考这个:

额,说起来我从来没用过 Web 选择 这个功能。

谢谢分享!

试用了下,交互思路差不多,这个脚本重心偏向框选链接,在 Chrome 上试的有些小 bug,看更新时间作者似乎断更了,有点可惜

确实小众,分享到这很合适:joy:

这个功能用来对付一些禁止复制的页面内容倒是很合适,以及少数情况下能恰好选中想要的内容,过滤掉不要的(比如选中代码而不选中行号)。不过我倒也确实没怎么用过。