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.2
// @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;

    /**
     * 元素选择器
     * @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 {string | ArrayBuffer | ArrayBufferView} data 
     * @param {string} type
     * @returns {ClipboardItem} 
     */
    function to_clipboarditem(data, type) {
        const blob = new Blob([data], { type: type });
        const item = new ClipboardItem({ [type]: blob });
        return item;
    }

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

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

    /**
     * 复制表格为纯文本到剪贴板
     * @param {HTMLTableElement} table 
     * @returns {Promise<ClipboardItem>} 
     */
    async function copy_table_as_text(table) {
        console.log("table to text");

        // table 转 tsv 格式文本
        const data = table_to_tsv(table);

        console.log(data);
        return to_clipboarditem(data, "text/plain");
    }

    /**
     * 复制表格为html到剪贴板
     * @param {HTMLTableElement} table 
     * @returns {Promise<ClipboardItem>} 
     */
    async function copy_table_as_html(table) {
        console.log("table to html");

        const _table = table.cloneNode(true);
        adjust_table(_table);
        return to_clipboarditem(_table.outerHTML, "text/html");
    }

    /**
     * 复制表格为图片到剪贴板
     * @param {HTMLTableElement} table 
     * @returns {Promise<ClipboardItem>} 
     */
    async function copy_table_as_image(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;

        const type = "image/png";
        const blob = await canvas_to_blob(canvas, type);
        return to_clipboarditem(blob, type);
    }

    /**
     * 使用过时的 execCommand 复制文本
     * @param {string} text 
     */
    function old_copy(text) {
        const input = document.createElement("input");
        input.value = text;
        document.body.append(input);
        input.select();
        document.execCommand("copy");
        input.remove();
    }

    /**
     * 以多种类型复制表格
     * @param {HTMLTableElement} table 
     */
    async function copy_table_as_multi_types(table) {
        const actions = [
            copy_table_as_text,
            copy_table_as_html,
            copy_table_as_image,
        ];
        const items = await gather(actions.map(
            copy => copy(table)
        ));

        try {
            const last_item = items.pop();

            for (const item of items) {
                await navigator.clipboard.write([item]);
                await sleep(COPY_GAP);
            }
            await navigator.clipboard.write([last_item]);
            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_multi_types(table);
        } else {
            old_copy(table_to_tsv(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, 1000);
})();