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.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);
})();