table-copier

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

目前為 2023-06-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name            table-copier
// @namespace       http://tampermonkey.net/
// @version         0.1
// @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)
        );
    }


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

        // table 转 tsv 格式文本
        const data = [...table.rows].map(
            row => [...row.cells].map(
                cell => cell.textContent.replace(/\t/g, "    ")
            ).join("\t")
        ).join("\n");

        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);
        }
        if (!canvas) return;

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

    /**
     * 复制表格到剪贴板
     * @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()
        );

        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("复制失败!");
        }

        // 增加按钮
        add_btns();
    }

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

    (async () => {
        try {
            await until_scripts_loaded();
        } catch(e) {
            console.error(e);
        }

        window.copy_table = copy_table;
        setTimeout(add_btns, 1000);
    })();
})();