iconfont一键复制SVG

适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。

目前為 2023-04-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name         iconfont一键复制SVG
// @namespace    http://tampermonkey.net/
// @version      1.0.3
// @description  适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。
// @author       [email protected]
// @match        https://www.iconfont.cn/search*
// @match        https://www.iconfont.cn/collections*
// @match        https://www.iconfont.cn/illustrations*
// @match        https://make.chuangyi.taobao.com/tools/illus?*
// @require      https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.7.3/sweetalert2.all.min.js
// @icon         http://img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg
// @grant        none
// @run-at       document-idle
// @note         1. 修正了网址匹配模式
// @note         2. 增加了 SVG_SIZE 全局常量便于修改尺寸
// @license      GPL-3.0-only
// ==/UserScript==


(async function() {
    "use strict";


    // 脚本级全局常量
    const SVG_SIZE = 20;
    const SMALL_DELAY = 200;
    const MEDIUM_DELAY = 500;
    const LARGE_DELAY = 1000;
    const XML = new XMLSerializer();


    /**
     * 工具类 ----------------------------------------------------------------------------
     */

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

    const util = {
        /**
         * 将 svg 元素序列化为大小为 20x20 的 svg 代码
         * @param {SVGElement} svg 
         * @returns {string}
         */
        svgToStr(svg) {
            // 设置大小
            svg.setAttribute("width", `${SVG_SIZE}`);
            svg.setAttribute("height", `${SVG_SIZE}`);

            // 序列化
            return XML.serializeToString(svg);
        },

        /**
         * 元素选择器
         * @param {string} selector 选择器
         * @returns {HTMLElement | null} 元素
         */
        $(selector) {
            const self = this?.querySelectorAll ? this : document;
            return self.querySelector(selector);
        },

        /**
         * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
         * @param {string} selector 选择器
         * @returns {Promise<Array<HTMLElement>>} 元素列表
         */
        async $$(selector) {
            const self = this?.querySelectorAll ? this : document;

            for (let i = 0; i < 10; i++) {
                let elems = [...self.querySelectorAll(selector)];
                if (elems.length > 0) {
                    return elems;
                }
                await sleep(MEDIUM_DELAY);
            }
            throw Error(`"${selector}" not found in 5s`);
        },

        /**
         * 基于 window.postMessage 通信的套接字对象
         */
        socket: class Socket {
            /**
             * 创建套接字对象
             * @param {Window} target 目标窗口
             */
            constructor(target) {
                if (!(target.window && (target === target.window))) {
                    console.log(target);
                    throw new Error(`target is not a [Window Object]`); 
                }
                this.target = target;
                this.connected = false;
                this.listeners = new Set();
            }
        
            get [Symbol.toStringTag]() { return "Socket"; }
        
            /**
             * 向目标窗口发消息
             * @param {*} message 
             */
            talk(message) {
                if (!this.target) {
                    throw new TypeError(
                        `socket.target is not a window: ${this.target}`
                    );
                }
                this.target.postMessage(message, "*");
            }
        
            /**
             * 添加捕获型监听器,返回实际添加的监听器
             * @param {Function} listener (e: MessageEvent) => {...}
             * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
             * @returns {Function} listener
             */
            listen(listener, once=false) {
                if (this.listeners.has(listener)) return;
        
                let real_listener = listener;
                // 包装监听器
                if (once) {
                    const self = this;
                    function wrapped(e) {
                        listener(e);
                        self.notListen(wrapped);
                    }
                    real_listener = wrapped;
                }
                
                // 添加监听器
                this.listeners.add(real_listener);
                window.addEventListener(
                    "message", real_listener, true
                );
                return real_listener;
            }
        
            /**
             * 移除socket上的捕获型监听器
             * @param {Function} listener (e: MessageEvent) => {...}
             */
            notListen(listener) {
                console.log(listener);
                console.log(
                    "listener delete operation:",
                    this.listeners.delete(listener)
                );
                window.removeEventListener("message", listener, true);
            }
        
            /**
             * 检查对方来信是否为pong消息
             * @param {MessageEvent} e 
             * @param {Function} resolve 
             */
            _onPong(e, resolve) {
                // 收到pong消息
                if (e.data.pong) {
                    this.connected = true;
                    this.listeners.forEach(
                        listener => listener.ping ? this.notListen(listener) : 0
                    );
                    console.log("Client: Connected!\n" + new Date());
                    resolve(this);
                }
            }
        
            /**
             * 向对方发送ping消息
             * @returns {Promise<Socket>}
             */
            _ping() {
                return new Promise((resolve, reject) => {
                    // 绑定pong检查监听器
                    const listener = this.listen(
                        e => this._onPong(e, resolve)
                    );
                    listener.ping = true;
        
                    // 5分钟后超时
                    setTimeout(
                        () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
                        5 * 60 * 1000
                    );
                    // 发送ping消息
                    this.talk({ ping: true });
                });
            }
        
            /**
             * 检查对方来信是否为ping消息
             * @param {MessageEvent} e 
             * @param {Function} resolve 
             */
            _onPing(e, resolve) {
                // 收到ping消息
                if (e.data.ping) {
                    this.target = e.source;
                    this.connected = true;
                    this.listeners.forEach(
                        listener => listener.pong ? this.notListen(listener) : 0
                    );
                    console.log("Server: Connected!\n" + new Date());
                    
                    // resolve 后期约状态无法回退
                    // 但后续代码仍可执行
                    resolve(this);
                    // 回应pong消息
                    this.talk({ pong: true });
                }
            }
        
            /**
             * 当对方来信是为ping消息时回应pong消息
             * @returns {Promise<Socket>}
             */
            _pong() {
                return new Promise(resolve => {
                    // 绑定ping检查监听器
                    const listener = this.listen(
                        e => this._onPing(e, resolve)
                    );
                    listener.pong = true;
                });
            }
        
            /**
             * 连接至目标窗口
             * @param {boolean} talk_first 是否先发送ping消息
             * @param {Window} target 目标窗口
             * @returns {Promise<Socket>}
             */
            connect(talk_first) {
                // 先发起握手
                if (talk_first) {
                    return this._ping();
                }
                // 后发起握手
                return this._pong();
            }
        }
    }


    /**
     * 域名主函数 ----------------------------------------------------------------------
     */


    /**
     * chuangyi 主函数
     */
    async function chuangyi() {
        console.log("进入 chuangyi");

        // 域名级全局常量
        const SVG_S = ".seditor-template__path__content svg";

        
        while (true) {
            if (util.$(SVG_S)) break;
            await sleep(SMALL_DELAY);
        }
        console.log("SVG 出现了,开始执行任务");


        async function main() {
            console.log("进入 chuangyi.main");

            const sock = new util.socket(window.top);
            await sock.connect(true);
            sock.talk({
                action: "copy-svg",
                svg: util.svgToStr(util.$(SVG_S))
            });
            
            console.log(Date.now());
            console.log("SVG 代码已经从 iframe 中发出!");
        }


        main();
    }


    /**
     * iconfont 主函数
     */
    async function iconfont () {
        console.log("进入 iconfont");

        // 域名级全局常量
        const STYLE_TEXT = `
            .force-hide {
                visibility: hidden !important;
            }

            .block-icon-list li:hover div.icon-cover {
                display: grid;
                grid-template-columns: auto auto;
            }

            .block-icon-list li .icon-cover span.cover-item-line {
                height: auto;
                line-height: 50px;
            }

            .svg-copy.disabled:hover {
                color: #6d6d6d !important;
            }

            .icon-fuzhidaima:before {
                font-size: 24px;
            }

            .copy-icon {
                border: none !important;
                margin: 0 1.25em !important;
                margin: 0 0 0 10px !important;
            }

            .copy-container {
                margin: 8px 16px !important;
                padding: 0 !important;
                font-size: 14px !important;
                
            }
            
            .copy-popup {
                top: 60px;
                padding: 4px 10px !important;
                height: 44px !important;
                font-size: 12px !important;
                width: fit-content !important;
                align-content: center;
                box-shadow: rgba(0, 0, 0, 0.2) 0px 12px 28px 0px, rgba(0, 0, 0, 0.1) 0px 2px 4px 0px, rgba(255, 255, 255, 0.05) 0px 0px 0px 1px inset !important;
            }

            .swal2-popup {
                border-radius:0 !important;    
            }
        `;


        // 域名级全局变量
        let sock;


        /**
         * 阻塞直到图标区存在
         */
        while (true) {
            if (util.$(".block-icon-list > li")) break;
            await sleep(SMALL_DELAY);
        }
        console.log("图标区出现了,开始执行任务");


        /**
         * 使用xhr异步GET请求目标url,返回响应体blob
         * @param {string} url 
         * @returns {Promise<Blob>} blob
         */
        async function xhrGetBlob(url) {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "blob";
            
            return new Promise((resolve, reject) => {
                xhr.onload = () => {
                    const code = xhr.status;

                    if (code >= 200 && code <= 299) 
                        resolve(xhr.response);

                    else reject(new Error(`Network Error: ${code}`));
                }
                xhr.send();
            });
        }


        /**
         * 加载CDN脚本
         * @param {string} url 
         */
        async function loadWebScript(url) {
            try {
                // xhr+eval方式
                Function(
                    await (await xhrGetBlob(url)).text()
                )();
            } catch(e) {
                console.error(e);
                // 嵌入<script>方式
                const script = document.createElement("script");
                script.src = url;
                document.body.append(script);
            }
        }


        function addStyle() {
            const id = "iconfont-svg-copy-style";
            if (util.$(`#${id}`)) return;

            const style = document.createElement("style");
            style.id = id;
            style.innerHTML = STYLE_TEXT;
            document.head.append(style);
        }


        function showCopySuccess() {
            Swal.fire({
                text: "复制成功,可以粘贴咯~",
                toast: true,
                timer: 2000,
                showConfirmButton: false,
                icon: "success",
                position: "top",
                customClass: {
                    popup: "copy-popup",
                    htmlContainer: "copy-container",
                    icon: "copy-icon"
                }
            });
        }


        /**
         * iframe 返回的 svg string 处理函数
         * @param {MessageEvent} e 
         */
        function onPopupDataReceived(e) {
            if (e.data.action !== "copy-svg") {
                return;
            }

            console.log(Date.now());
            console.log("iframe SVG 代码已经收到!");
            // 复制 svg string
            copyText(e.data.svg);
            // 关闭弹窗
            util.$(".mp-e2e-dialog-close").click();
            // 移除监听器
            sock.notListen(onPopupDataReceived);
        }


        /**
         * 在 iframe 中获取 svg
         * @param {HTMLElement} card 
         */
        async function copyInIframe(card) {
            // 禁用复制按钮
            const icon = util.$.call(card, ".svg-copy");
            icon.classList.add("disabled");
            icon.removeEventListener("click", copySVGElement, true);
            
            // 触发弹窗
            const download = util.$.call(card, "[title='下载']");
            download.click();
            
            // 等待弹窗加载完毕
            while (true) {
                if (util.$("[id*='dlg_']") && util.$("[id*='mask_dlg_']")) {
                    break;
                }
                await sleep(SMALL_DELAY);
            }
            
            // 隐藏弹窗
            const dialogs = await util.$$("[id*='dlg_']");
            dialogs.forEach(
                elem => elem.classList.add("force-hide")
            );

            let popup;
            for (let elem of dialogs) {
                if (elem.id.startsWith("dlg_")) {
                    popup = elem;
                }
            }
            if (!popup) throw new Error("#dlg_ not found");
            
            // 通讯 iframe 等待接收 svg 字符串
            const iframes = await util.$$.call(popup, "iframe");
            sock = new util.socket(
                iframes[0].contentWindow
            )
            // 当收到 svg string 时复制到剪贴板
            sock.listen(onPopupDataReceived);
            await sock.connect(false);

            // 复制完成后再启用复制按钮
            await sleep(0);
            icon.classList.remove("disabled");
            icon.addEventListener("click", copySVGElement, true);
        }


        async function copyText(text) {
            // 复制到剪贴板
            try {
                await navigator.clipboard.writeText(text);
            } catch (err) {
                console.error(err);
                console.log(text);
            }
            // 提示复制成功
            showCopySuccess();
        }


        /**
         * 当点击复制图标时复制 svg 到剪贴板
         * @param {PointerEvent} event 
         */
        function copySVGElement(event) {
            // 取得svg
            const card = event.target.closest("li");
            const svg = card.querySelector("svg");

            // 如果是在 iframe 中的,那就要通过模拟点击下载的方式来获取
            if (!svg) {
                copyInIframe(card);
                return;
            }

            // 序列化
            const svg_str = util.svgToStr(svg);
            copyText(svg_str);
        }


        /**
         * 导入 sweet alert 2
         * @returns {Promise<void>}
         */
        function importSweetAlert() {
            if (!window.Swal) {
                return loadWebScript(
                    "https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.7.3/sweetalert2.all.min.js"
                );
            }
            return Promise.resolve();
        }

        function addCopyIcon() {
            // 获取卡片
            const cards = [...document.querySelectorAll(".block-icon-list > li")];
            if (!cards[0]) throw new Error("无法选中图标块");
            
            // 制作按钮元素模板
            const template = document.createElement("span");
            template.title = "复制SVG";
            template.classList.add(
                "cover-item", "iconfont", "cover-item-line", "icon-fuzhidaima", "svg-copy"
            );

            cards.forEach(card => {
                // 添加复制图标
                const icon_copy = template.cloneNode();
                // 增加复制功能
                icon_copy.addEventListener("click", copySVGElement, true);
                card.querySelector(".icon-cover").append(icon_copy);
            });
        }


        async function mainTask() {
            console.log("mainTask entered");

            // 无关路径
            const path = location.pathname;
            if (!(path.startsWith("/illustrations/") ||
                path.startsWith("/collections/")
            )) return;
            
            // 等待直到图标块出现
            while (true) {
                if (util.$(".block-icon-list > li")) break;
                await sleep(SMALL_DELAY);
            }

            // 如果已经存在按钮,退出主函数
            if (util.$(".icon-cover span.svg-copy")) return;
            console.log("正在建造 [复制SVG] 图标...");

            addStyle();
            addCopyIcon();

            // 导入 sweet alert
            importSweetAlert().catch(err => {
                console.error(err);
                console.log("sweet alert 导入失败,没有提示弹窗了");
            });

            console.log("[复制SVG] 图标 建造完成");
        }

        function delayedTask() {
            setTimeout(mainTask, 0);
        }


        function getIconsBox() {
            const s = ".block-icon-list";
            return util.$(`${s} li`).closest(s);
        }


        function monitorIconsChanging() {
            const observer = new MutationObserver(delayedTask);
            observer.observe(
                getIconsBox(), { childList: true }
            );
        }


        const onMainChanged = (function() {
            let icons_box = getIconsBox();

            function inner() {
                const new_box = getIconsBox();
                if (icons_box === new_box) return;

                icons_box = new_box;
                mainTask();
                monitorIconsChanging();
            }

            function delayed() {
                setTimeout(inner, LARGE_DELAY);
            }

            return delayed;
        })();


        async function monitorMainChanging() {
            const elem = (await util.$$("#magix_vf_main"))[0];
            const observer = new MutationObserver(onMainChanged);
            observer.observe(elem, { attributes: true });
        }


        function main() {
            console.log("进入 iconfont.main");
            mainTask();
            monitorMainChanging();
            monitorIconsChanging();
            window.addEventListener("popstate", mainTask, true);
        }
        

        main();
    }


    /**
     * 路由,主函数入口
     */
    (function route() {
        console.log("进入 route");
        const host = location.hostname;

        switch (host) {
            case "www.iconfont.cn":
                iconfont();
                break;
        
            case "make.chuangyi.taobao.com":
                chuangyi();
                break;

            default:
                console.log(`未知域名,不能处理:${host}`);
                break;
        }
    })();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址