table-copier

适用于任意网站,快速复制表格为纯文本、HTML、图片

当前为 2023-06-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            table-copier
// @namespace       http://tampermonkey.net/
// @version         0.3
// @description     适用于任意网站,快速复制表格为纯文本、HTML、图片
// @match           *://*/*
// @require         https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js
// @grant           none
// @run-at          document-idle
// @license         GPL-3.0-only
// @create          2023-06-27
// ==/UserScript==


(function() {
    "use strict";

    const SCRIPTS = [
        ["html2canvas", "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js"]
    ];
    const BTN = `<button class="never-gonna-give-you-up" style="width: 70px; height: 30px;" onclick="copy_table(this)">复制表格</button>`;
    const COPY_GAP = 500;
    const BOOT_DELAY = 2000;

    /**
     * 元素选择器
     * @param {string} selector 
     * @returns {Array<HTMLElement>}
     */
    function $(selector) {
        const self = this?.querySelectorAll ? this : document;
        return [...self.querySelectorAll(selector)];
    }

    /**
     * 异步等待delay毫秒
     * @param {number} delay 
     * @returns 
     */
    function sleep(delay) {
        return new Promise(resolve => setTimeout(resolve, delay));
    }

    /**
     * 加载CDN脚本
     * @param {string} url 
     */
    async function load_script(url) {
        try {
            const code = await (await fetch(url)).text();
            Function(code)();
        } catch(e) {
            return new Promise(resolve => {
                console.error(e);
                // 嵌入<script>方式
                const script = document.createElement("script");
                script.src = url;
                script.onload = resolve;
                document.body.appendChild(script);
            });
        }
    }

    async function until_scripts_loaded() {
        return gather(SCRIPTS.map(
            // kv: [prop, url]
            kv => (async () => {
                if (window[kv[0]]) return;
                await load_script(kv[1]);
            })()
        ));
    }

    /**
     * 等待全部任务落定后返回值的列表
     * @param {Iterable<Promise>} tasks 
     * @returns {Promise<Array>} values
     */
    async function gather(tasks) {
        const results = await Promise.allSettled(tasks);
        const filtered = [];
        for (const result of results) {
            if (result.value) {
                filtered.push(result.value);
            }
        }
        return filtered;
    }

    /**
     * 递归的修正表内元素
     * @param {HTMLElement} elem 
     */
    function adjust_table(elem) {
        for (const child of elem.children) {
            adjust_table(child);

            for (const attr of child.attributes) {
                // 链接补全
                const name = attr.name;
                if (["src", "href"].includes(name)) {
                    child.setAttribute(name, child[name]);
                }
            }
        }
    }

    /**
     * @param {Blob} blob
     * @returns {ClipboardItem} 
     */
    function blob_to_item(blob) {
        return new ClipboardItem({ [blob.type]: blob });
    }

    /**
     * canvas转blob
     * @param {HTMLCanvasElement} canvas 
     * @param {string} type
     * @returns {Promise<Blob>}
     */
    function canvas_to_blob(canvas) {
        return new Promise(
            resolve => canvas.toBlob(resolve, "image/png")
        );
    }

    /**
     * 表格转tsv字符串
     * @param {HTMLTableElement} table 
     */
    function table_to_tsv(table) {
        return [...table.rows].map(
            row => [...row.cells].map(
                cell => cell
                    .textContent
                    .replace(/\n/g, "")
                    .replace(/\t/g, "    ")
                    .trim()
            ).join("\t")
        ).join("\n");
    }

    /**
     * @param {HTMLTableElement} table 
     * @returns {Promise<Blob>} 
     */
    async function table_to_text_blob(table) {
        console.log("table to text");
        // table 转 tsv 格式文本
        const text = table_to_tsv(table);
        console.log(text);
        return new Blob([text], { type: "text/plain" });
    }

    /**
     * @param {HTMLTableElement} table 
     * @returns {Promise<Blob>} 
     */
    async function table_to_html_blob(table) {
        console.log("table to html");

        const _table = table.cloneNode(true);
        adjust_table(_table);
        return new Blob([_table.outerHTML], { type: "text/html" });
    }

    /**
     * @param {HTMLTableElement} table 
     * @returns {Promise<Blob>} 
     */
    async function table_to_image_blob(table) {
        console.log("table to image");

        let canvas;
        try {
            canvas = await window.html2canvas(table);
        } catch(e) {
            console.error(e);
        }
        console.log("canvas:", canvas);
        if (!canvas) return;

        return canvas_to_blob(canvas);
    }

    /**
     * 使用过时的 execCommand 复制文本
     * @param {string} text 
     * @returns {Promise<string>}
     */
    async function old_copy(text) {
        return new Promise(resolve => {
            document.oncopy = event => {
                event.clipboardData.setData("text/plain", text);
                event.preventDefault();
                resolve();
            };
            document.execCommand("copy");
        });
    }

    /**
     * @param {Blob} blob 
     * @returns {Promise<void>}
     */
    function copy(blob) {
        const item = blob_to_item(blob);
        return navigator.clipboard.write([item]);
    }
    
    /**
     * @param {HTMLTableElement} table
     * @returns {Promise<void>} 
     */
    async function copy_table_as_multi_types(table) {
        const converts = [
            table_to_text_blob,
            table_to_html_blob,
            table_to_image_blob,
        ];
        
        const blobs = await gather(converts.map(
            convert => convert(table)
        ));

        try {
            const last_blob = blobs.pop();

            for (const blob of blobs) {
                await copy(blob);
                await sleep(COPY_GAP);
            }
            await copy(last_blob);
            alert("复制成功!");

        } catch(e) {
            console.error(e);
            alert("复制失败!");
        }
    }

    /**
     * @param {HTMLTableElement} table
     * @returns {Promise<void>} 
     */
    async function copy_table_as_text(table) {
        try {
            await old_copy(table_to_tsv(table));
            alert("复制成功!");
        } catch(e) {
            console.error(e);
            alert("复制失败!");
        }
    }

    /**
     * 复制表格到剪贴板
     * @param {HTMLButtonElement} btn
     */
    async function copy_table(btn) {
        const table = btn.closest("table");
        if (!table) {
            alert("出错了:按钮外部没有表格");
            return;
        }

        // 移除按钮
        $(".never-gonna-give-you-up").forEach(
            btn => btn.remove()
        );
        // 复制表格
        if (!navigator.clipboard) {
            await copy_table_as_text(table);
        } else {
            await copy_table_as_multi_types(table);
        }
        // 增加按钮
        add_btns();
    }

    function add_btns() {
        for (const table of $("table")) {
            // 跳过隐藏的表格
            if (!table.getClientRects()[0]) continue;
            table.insertAdjacentHTML("afterbegin", BTN);
        }
    }

    async function main() {
        try {
            await until_scripts_loaded();
        } catch(e) {
            console.error(e);
        }

        window.copy_table = copy_table;
        add_btns();

        // 递归的注入自身到iframe
        $("iframe").forEach(iframe => {
            try {
                iframe.contentWindow.eval(main.toString());
            } catch(e) {}
        });
    };

    setTimeout(main, BOOT_DELAY);
})();