- // ==UserScript==
- // @name iconfont一键复制SVG
- // @namespace http://tampermonkey.net/
- // @version 1.0.3
- // @description 适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。
- // @author 2690874578@qq.com
- // @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;
- }
- })();
- })();