自动复制脚本在飞书妙记页面不生效

JS 小白,写了个自动复制脚本,在别的网站上用的好好的,但是在飞书妙记页面却不管用
看控制台记录是有脚本代码中的 log “应当复制”的
另外 MDN 说 document.execCommand 要废弃了,但感觉新出的那个 Clipboard API 又没那么傻瓜化,好难啊。假如我选中的内容又有图片又有文本,该咋用呢?
Clipboard: write() method - Web APIs | MDN (mozilla.org)

飞书妙记页面举例:
05-31 | 图书协会周三分享会-“拿捏”用户的艺术:如何创作出让人惊叹的知识内容 (larkoffice.com)

脚本代码:只要选中的元素不在文本框之类的元素中就自动复制

// ==UserScript==
// @name         AutoCopy
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @include      *
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAABpElEQVR4nO3Vv2uUQRDG8c/ebSMWqay0trATAxrUSi1S2AiWFoJYpNCgoBjURsHWJKeNRfAvsDgFixQqKdPZ2ViEiCJYBOQu8f1hEXO59713j7MUfLZ6d2a/O8vMO0OzDnin9Ku2Mjvuaw07xgSAYEVXe2indMhj92zpKJLnBhF8MDeye9hn6zbN70eRiqCw02Bra3up8BBLu1FEBxsBucXqW4csz0ULe4jorSCMuPU89boRELDMHiI6Y8V65bbCUTccc70RkaOwKLOg0IkyXa9qTjOu2LAs6NZuD86hrdTyxRNTkUqqdhXlHrngGRVEZsMpJwex9DxIZSHYclesIb65LCoHgIs66UJq6btDBZHZrPh8V6YBOX66LbOkTGckBYimBW2FVTNeuOZNyrFJ236Yl4NSy5SbVm1PDvhodqgyMledTdRlAtDzqfL9tfkwUtyaRkv9LwFj9B/w7wPycXOhqlJ0yZHKPChMi5MCiM47XhsopbVJAUHfrYbmN/EToN+02eLPfz9OYyZhFJzW1Jn3lTsxaKQjCkp52jy45r1ZvSbTb9M0d4PBozGZAAAAAElFTkSuQmCC
// ==/UserScript==

let mouseDownTarget;

window.onmousedown = function (e) {
    mouseDownTarget = e.target;
};

window.onmouseup = function () {
    if (!inTextBox()) {
        console.log('应当复制');
        document.execCommand('copy');
    }
};

function inTextBox () {
    let ref = [],
        general = ['textarea, input, *[contenteditable="true"]'];
    for (let i of general) {
        let t = document.querySelectorAll(i);
        if (t.length != 0)
            t.forEach(j => {
                ref.push(j);
            });
    }
    rules = {
        'mail.google.com': 'div[aria-label= "Message Body"]',
        'outlook.live.com': '#editorParent_1',
    };
    for (let i in rules) {
        if (i == document.location.hostname) {
            let t = document.querySelectorAll(rules[i]);
            if (t.length != 0)
                t.forEach(j => {
                    ref.push(j);
                });
        }
    }
    for (let i = 0; i < ref.length; i++)
        if (ref[i].contains(mouseDownTarget)) return true;
    return false;
}

// @grant GM_setClipboard

可以用这个 API

我看了下 Documentation | Tampermonkey,似乎这个只能复制字符串?但选中的内容里经常还包含图片,这样不就没法复制了吗?还是用 execCommand 方便啊,可惜就是不知道为啥唯独不能在飞书妙记上生效

另外我试了把 document.execCommand('copy'); 换成以下代码,但只有第一种才能正常复制

  1. GM_setClipboard 函数的第二个参数用 text
let content = window.getSelection().toString();
GM_setClipboard(content, "text");
  1. GM_setClipboard 函数的第二个参数用 html
let content = window.getSelection().toString();
GM_setClipboard(content, "html");

突然想@dms :joy: 求助!
————
咋 at 不高亮呢 :cold_face:

不是跟你说过吗,用 UC 脚本,一个浏览器级别的 goDoCommand('cmd_copy') 就能完成,而且通杀,或者扩展不用自己动脑

前端不太懂,可能是浏览器安全策略问题,非输入框这个命令不生效,只能新建一个 textarea 然后对 textarea 执行 execCommand

这就涉及到到获取原来的 html 以及原来的样式

第二种也能正常复制,但是只能在 Word 这样的富文本编辑器里粘贴,不知道是不是 BUG

然后我尝试了一下新 API,Firefox 默认没有启用 ClipboardItem

dom.events.asyncClipboard.clipboardItem → true

下面是我弄得测试脚本,getSelection 不是你的需要,我随便找的一个函数

// ==UserScript==
// @name         AutoCopy
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @include      *
// @grant        GM_setClipboard
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAABpElEQVR4nO3Vv2uUQRDG8c/ebSMWqay0trATAxrUSi1S2AiWFoJYpNCgoBjURsHWJKeNRfAvsDgFixQqKdPZ2ViEiCJYBOQu8f1hEXO59713j7MUfLZ6d2a/O8vMO0OzDnin9Ku2Mjvuaw07xgSAYEVXe2indMhj92zpKJLnBhF8MDeye9hn6zbN70eRiqCw02Bra3up8BBLu1FEBxsBucXqW4csz0ULe4jorSCMuPU89boRELDMHiI6Y8V65bbCUTccc70RkaOwKLOg0IkyXa9qTjOu2LAs6NZuD86hrdTyxRNTkUqqdhXlHrngGRVEZsMpJwex9DxIZSHYclesIb65LCoHgIs66UJq6btDBZHZrPh8V6YBOX66LbOkTGckBYimBW2FVTNeuOZNyrFJ236Yl4NSy5SbVm1PDvhodqgyMledTdRlAtDzqfL9tfkwUtyaRkv9LwFj9B/w7wPycXOhqlJ0yZHKPChMi5MCiM47XhsopbVJAUHfrYbmN/EToN+02eLPfz9OYyZhFJzW1Jn3lTsxaKQjCkp52jy45r1ZvSbTb9M0d4PBozGZAAAAAElFTkSuQmCC
// ==/UserScript==

(function () {
    'use strict';

    const AutoCopy = {
        mouseDownTarget: null,

        init() {
            window.addEventListener('mousedown', this.onMouseDown.bind(this));
            window.addEventListener('mouseup', this.onMouseUp.bind(this));
        },

        onMouseDown(e) {
            this.mouseDownTarget = e.target;
        },

        async onMouseUp() {
            if (!this.inTextBox()) {
                console.log('应当复制');
                try {
                    this.copyTextWithFormatting();
                    console.log('复制成功');
                } catch (err) {
                    console.error('复制失败', err);
                }
            }
        },

        inTextBox() {
            const ref = [];
            const general = ['textarea', 'input', '*[contenteditable="true"]'];
            general.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                elements.forEach(element => ref.push(element));
            });

            const rules = {
                'mail.google.com': 'div[aria-label="Message Body"]',
                'outlook.live.com': '#editorParent_1',
            };

            for (const domain in rules) {
                if (domain === document.location.hostname) {
                    const elements = document.querySelectorAll(rules[domain]);
                    elements.forEach(element => ref.push(element));
                }
            }

            return ref.some(element => element.contains(this.mouseDownTarget));
        },

        copyTextWithFormatting() {
            const richTextObj = this.getSelection();
            if (!richTextObj) return;
            const plainText = this.getPlainText(richTextObj.html);
            console.log(plainText, richTextObj.html)
            const clipboardItem = new ClipboardItem({
                "text/plain": new Blob(
                    [plainText],
                    { type: "text/plain" }
                ),
                "text/html": new Blob(
                    [richTextObj.html],
                    { type: "text/html" }
                ),
            });

            navigator.clipboard.write([clipboardItem]);
        },

        getPlainText(html) {
            const tempDiv = document.createElement("div");
            tempDiv.innerHTML = html;
            return tempDiv.textContent || tempDiv.innerText || "";
        },

        getSelection() {
            // These are markers used to delimit the selection during processing. They
            // are removed from the final rendering.
            // We use noncharacter Unicode codepoints to minimize the risk of clashing
            // with anything that might legitimately be present in the document.
            // U+FDD0..FDEF <noncharacters>
            const MARK_SELECTION_START = "\uFDD0";
            const MARK_SELECTION_END = "\uFDEF";
            ;
            var selection = window.getSelection();

            var range = selection.getRangeAt(0);
            var ancestorContainer = range.commonAncestorContainer;
            var doc = ancestorContainer.ownerDocument;

            var startContainer = range.startContainer;
            var endContainer = range.endContainer;
            var startOffset = range.startOffset;
            var endOffset = range.endOffset;

            // let the ancestor be an element
            var Node = doc.defaultView.Node;
            if (
                ancestorContainer.nodeType == Node.TEXT_NODE ||
                ancestorContainer.nodeType == Node.CDATA_SECTION_NODE
            ) {
                ancestorContainer = ancestorContainer.parentNode;
            }

            // for selectAll, let's use the entire document, including <html>...</html>
            // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
            try {
                if (ancestorContainer == doc.body) {
                    ancestorContainer = doc.documentElement;
                }
            } catch (e) { }

            // each path is a "child sequence" (a.k.a. "tumbler") that
            // descends from the ancestor down to the boundary point
            var startPath = this.getPath(ancestorContainer, startContainer);
            var endPath = this.getPath(ancestorContainer, endContainer);

            // clone the fragment of interest and reset everything to be relative to it
            // note: it is with the clone that we operate/munge from now on.  Also note
            // that we clone into a data document to prevent images in the fragment from
            // loading and the like.  The use of importNode here, as opposed to adoptNode,
            // is _very_ important.
            // XXXbz wish there were a less hacky way to create an untrusted document here
            var isHTML = doc.createElement("div").tagName == "DIV";
            var dataDoc = isHTML
                ? ancestorContainer.ownerDocument.implementation.createHTMLDocument("")
                : ancestorContainer.ownerDocument.implementation.createDocument(
                    "",
                    "",
                    null
                );
            ancestorContainer = dataDoc.importNode(ancestorContainer, true);
            startContainer = ancestorContainer;
            endContainer = ancestorContainer;

            // Only bother with the selection if it can be remapped. Don't mess with
            // leaf elements (such as <isindex>) that secretly use anynomous content
            // for their display appearance.
            var canDrawSelection = ancestorContainer.hasChildNodes();
            var tmpNode;
            if (canDrawSelection) {
                var i;
                for (i = startPath ? startPath.length - 1 : -1; i >= 0; i--) {
                    startContainer = startContainer.childNodes.item(startPath[i]);
                }
                for (i = endPath ? endPath.length - 1 : -1; i >= 0; i--) {
                    endContainer = endContainer.childNodes.item(endPath[i]);
                }

                // add special markers to record the extent of the selection
                // note: |startOffset| and |endOffset| are interpreted either as
                // offsets in the text data or as child indices (see the Range spec)
                // (here, munging the end point first to keep the start point safe...)
                if (
                    endContainer.nodeType == Node.TEXT_NODE ||
                    endContainer.nodeType == Node.CDATA_SECTION_NODE
                ) {
                    // do some extra tweaks to try to avoid the view-source output to look like
                    // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
                    // To get a neat output, the idea here is to remap the end point from:
                    // 1. ...<tag>]...   to   ...]<tag>...
                    // 2. ...]</tag>...  to   ...</tag>]...
                    if (
                        (endOffset > 0 && endOffset < endContainer.data.length) ||
                        !endContainer.parentNode ||
                        !endContainer.parentNode.parentNode
                    ) {
                        endContainer.insertData(endOffset, MARK_SELECTION_END);
                    } else {
                        tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
                        endContainer = endContainer.parentNode;
                        if (endOffset === 0) {
                            endContainer.parentNode.insertBefore(tmpNode, endContainer);
                        } else {
                            endContainer.parentNode.insertBefore(
                                tmpNode,
                                endContainer.nextSibling
                            );
                        }
                    }
                } else {
                    tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
                    endContainer.insertBefore(
                        tmpNode,
                        endContainer.childNodes.item(endOffset)
                    );
                }

                if (
                    startContainer.nodeType == Node.TEXT_NODE ||
                    startContainer.nodeType == Node.CDATA_SECTION_NODE
                ) {
                    // do some extra tweaks to try to avoid the view-source output to look like
                    // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
                    // To get a neat output, the idea here is to remap the start point from:
                    // 1. ...<tag>[...   to   ...[<tag>...
                    // 2. ...[</tag>...  to   ...</tag>[...
                    if (
                        (startOffset > 0 && startOffset < startContainer.data.length) ||
                        !startContainer.parentNode ||
                        !startContainer.parentNode.parentNode ||
                        startContainer != startContainer.parentNode.lastChild
                    ) {
                        startContainer.insertData(startOffset, MARK_SELECTION_START);
                    } else {
                        tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
                        startContainer = startContainer.parentNode;
                        if (startOffset === 0) {
                            startContainer.parentNode.insertBefore(tmpNode, startContainer);
                        } else {
                            startContainer.parentNode.insertBefore(
                                tmpNode,
                                startContainer.nextSibling
                            );
                        }
                    }
                } else {
                    tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
                    startContainer.insertBefore(
                        tmpNode,
                        startContainer.childNodes.item(startOffset)
                    );
                }
            }

            // now extract and display the syntax highlighted source
            tmpNode = dataDoc.createElementNS("http://www.w3.org/1999/xhtml", "div");
            tmpNode.appendChild(ancestorContainer);

            return {
                isHTML: isHTML,
                html: tmpNode.innerHTML,
                drawSelection: canDrawSelection,
                baseURI: doc.baseURI,
            };
        },

        getPath(ancestor, node) {
            var n = node;
            var p = n.parentNode;
            if (n == ancestor || !p) {
                return null;
            }
            var path = [];
            if (!path) {
                return null;
            }
            do {
                for (var i = 0; i < p.childNodes.length; i++) {
                    if (p.childNodes.item(i) == n) {
                        path.push(i);
                        break;
                    }
                }
                n = p;
                p = n.parentNode;
            } while (n != ancestor && p);
            return path;
        }
    };

    AutoCopy.init();
})();

俺的主力浏览器是 Edge,没法用 UC 脚本啊
你这个脚本复制下来的格式都丢失掉了,跟按 Ctrl + C 的效果不一样
而且 Firefox 上的扩展都有点小毛病
CopyOnSelect、ACS-AutoCopyonSelect:不能长时间按着鼠标,不然不会复制
Auto Copy:跟上面的恰恰相反,必须得按着鼠标一小段时间,不能在松按键时复制

求助本版的大神 :smiling_face_with_tear:
@dms @hoothin

@符号前面空格就好了(显然后面你学会了

@18CM 说的没错,浏览器级别的命令确实能避免非常多的麻烦,很直接的解决问题。

GM_setClipboard

在油猴里我一般会使用 GM_setClipboard API 来解决问题,因为稳定好用。看起来好像可以复制 html,但是我没测试过,我一般都是回避富文本的。

这里的代码有点问题,toString() 获得的肯定是字符串,所以第二句无论如何工作复制到的也只能是文本。

但实际上这个 API 的描述我也没看懂。

document.execCommand

这个可以不用太在意,只要在你的浏览器上能工作就行。这话说了很多年了,但也还可以用。在飞书上不可用似乎是因为这个方法别网站重写了,倒是也有点办法绕过去,大概就是在这个方法被重写之前先拿到这个方法……(好麻烦

Clipboard

如果是纯文本,倒也挺简单的。

navigator.clipboard.writeText(document.getSelection().toString())

有一个问题,这个在使用时要求网页处于焦点(激活)状态,所以在控制台直接运行好像不太行。富文本我没弄过(一直在回避……

代码

可以尝试一下如下代码,我设置的是按住 Ctrl+鼠标右键会复制选中内容,似乎是可以的。(代码我加了注释,有问题可以继续讨论

async function copySelectedHtmlToClipboard() {
  // 获取用户当前选中的HTML内容
  const selection = window.getSelection();
  const range = selection.getRangeAt(0);
  const clonedSelection = range.cloneContents();
  // 创建一个div,将选中的内容添加到其中
  const div = document.createElement('div');
  div.appendChild(clonedSelection);
  // 获取选中内容的 HTML 代码和文本
  const htmlContent = div.innerHTML;
  const textContent = selection.toString()
  // 创建 Blob 对象
  const html = new Blob([htmlContent], { type: "text/html" });
  const text = new Blob([textContent], { type: "text/plain" });
  // 将 Blob 对象写入剪贴板
  const data = new ClipboardItem({ "text/html": html, "text/plain": text });
  await navigator.clipboard.write([data]);
}

document.oncontextmenu = (event) => {
  if(event.ctrlKey){
    event.preventDefault();
    copySelectedHtmlToClipboard()
  }
};
1 个赞

老鼠写代码越来越厉害了啊

Nice!好棒!学习了! :kissing_closed_eyes: :kissing_closed_eyes: :kissing_closed_eyes:
为啥一直回避富文本嘞?

冠冕堂皇的讲是考虑兼容性,实话实说是懒加上自己有点看不懂(上面的代码也是现买现卖的((逃

今天在 Firefox上 测试,发现居然报错,提示
Uncaught (in promise) ReferenceError: ClipboardItem is not defined
:smiling_face_with_tear:
在 Edge 上就没问题正常用啊

这个 API 火狐上好像还不支持•ᴗ•:droplet:

1 个赞

还真是 没想到在 MDN 看文档,Firefox 却仍没支持
还有老鼠起得好早 :+1:

最近睡眠质量奇差,呜呜呜

你根本就没有阅读我在上边写的文字,我都说

并且

没理解你说的不需要getSelection是什么意思,UC脚本和油猴脚本不都用到了吗?

之前测试的时候因为还在主力用Edge,就没细看。
现在回过头来用火狐,因为记得你说的是UC脚本,还是没细看 :smiling_face_with_tear: 感谢提醒