您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。
当前为
// ==UserScript== // @name iconfont一键复制SVG // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description 适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。 // @author [email protected] // @match https://www.iconfont.cn/* // @match https://make.chuangyi.taobao.com/tools/illus?* // @exclude https://www.iconfont.cn/illustrations_3d* // @exclude https://www.iconfont.cn/lotties* // @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. 移除 3D 插画库、Lottie 库的页面错误匹配 // @license GPL-3.0-only // ==/UserScript== (async function() { "use strict"; // 脚本级全局常量 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", "20"); svg.setAttribute("height", "20"); // 序列化 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; } .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 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); } 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", "icon-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"); // 等待直到图标块出现 while (true) { if (util.$(".block-icon-list > li")) break; await sleep(SMALL_DELAY); } // 如果已经存在按钮,退出主函数 if (util.$(".icon-cover span.icon-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; } })(); })();