iconfont一键复制SVG

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

目前为 2023-04-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name iconfont一键复制SVG
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.3
  5. // @description 适用于 iconfont 快速复制 SVG 代码。支持图标库文件复制,矢量插画库文件复制。默认复制图标为20px,方便调整。
  6. // @author 2690874578@qq.com
  7. // @match https://www.iconfont.cn/search*
  8. // @match https://www.iconfont.cn/collections*
  9. // @match https://www.iconfont.cn/illustrations*
  10. // @match https://make.chuangyi.taobao.com/tools/illus?*
  11. // @require https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.7.3/sweetalert2.all.min.js
  12. // @icon http://img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg
  13. // @grant none
  14. // @run-at document-idle
  15. // @note 1. 修正了网址匹配模式
  16. // @note 2. 增加了 SVG_SIZE 全局常量便于修改尺寸
  17. // @license GPL-3.0-only
  18. // ==/UserScript==
  19.  
  20.  
  21. (async function() {
  22. "use strict";
  23.  
  24.  
  25. // 脚本级全局常量
  26. const SVG_SIZE = 20;
  27. const SMALL_DELAY = 200;
  28. const MEDIUM_DELAY = 500;
  29. const LARGE_DELAY = 1000;
  30. const XML = new XMLSerializer();
  31.  
  32.  
  33. /**
  34. * 工具类 ----------------------------------------------------------------------------
  35. */
  36.  
  37. /**
  38. * 异步的等待 delay_ms 毫秒
  39. * @param {number} delay_ms
  40. * @returns {Promise<void>}
  41. */
  42. function sleep(delay_ms) {
  43. return new Promise(
  44. resolve => setTimeout(resolve, delay_ms)
  45. );
  46. }
  47.  
  48. const util = {
  49. /**
  50. * 将 svg 元素序列化为大小为 20x20 的 svg 代码
  51. * @param {SVGElement} svg
  52. * @returns {string}
  53. */
  54. svgToStr(svg) {
  55. // 设置大小
  56. svg.setAttribute("width", `${SVG_SIZE}`);
  57. svg.setAttribute("height", `${SVG_SIZE}`);
  58.  
  59. // 序列化
  60. return XML.serializeToString(svg);
  61. },
  62.  
  63. /**
  64. * 元素选择器
  65. * @param {string} selector 选择器
  66. * @returns {HTMLElement | null} 元素
  67. */
  68. $(selector) {
  69. const self = this?.querySelectorAll ? this : document;
  70. return self.querySelector(selector);
  71. },
  72.  
  73. /**
  74. * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
  75. * @param {string} selector 选择器
  76. * @returns {Promise<Array<HTMLElement>>} 元素列表
  77. */
  78. async $$(selector) {
  79. const self = this?.querySelectorAll ? this : document;
  80.  
  81. for (let i = 0; i < 10; i++) {
  82. let elems = [...self.querySelectorAll(selector)];
  83. if (elems.length > 0) {
  84. return elems;
  85. }
  86. await sleep(MEDIUM_DELAY);
  87. }
  88. throw Error(`"${selector}" not found in 5s`);
  89. },
  90.  
  91. /**
  92. * 基于 window.postMessage 通信的套接字对象
  93. */
  94. socket: class Socket {
  95. /**
  96. * 创建套接字对象
  97. * @param {Window} target 目标窗口
  98. */
  99. constructor(target) {
  100. if (!(target.window && (target === target.window))) {
  101. console.log(target);
  102. throw new Error(`target is not a [Window Object]`);
  103. }
  104. this.target = target;
  105. this.connected = false;
  106. this.listeners = new Set();
  107. }
  108. get [Symbol.toStringTag]() { return "Socket"; }
  109. /**
  110. * 向目标窗口发消息
  111. * @param {*} message
  112. */
  113. talk(message) {
  114. if (!this.target) {
  115. throw new TypeError(
  116. `socket.target is not a window: ${this.target}`
  117. );
  118. }
  119. this.target.postMessage(message, "*");
  120. }
  121. /**
  122. * 添加捕获型监听器,返回实际添加的监听器
  123. * @param {Function} listener (e: MessageEvent) => {...}
  124. * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
  125. * @returns {Function} listener
  126. */
  127. listen(listener, once=false) {
  128. if (this.listeners.has(listener)) return;
  129. let real_listener = listener;
  130. // 包装监听器
  131. if (once) {
  132. const self = this;
  133. function wrapped(e) {
  134. listener(e);
  135. self.notListen(wrapped);
  136. }
  137. real_listener = wrapped;
  138. }
  139. // 添加监听器
  140. this.listeners.add(real_listener);
  141. window.addEventListener(
  142. "message", real_listener, true
  143. );
  144. return real_listener;
  145. }
  146. /**
  147. * 移除socket上的捕获型监听器
  148. * @param {Function} listener (e: MessageEvent) => {...}
  149. */
  150. notListen(listener) {
  151. console.log(listener);
  152. console.log(
  153. "listener delete operation:",
  154. this.listeners.delete(listener)
  155. );
  156. window.removeEventListener("message", listener, true);
  157. }
  158. /**
  159. * 检查对方来信是否为pong消息
  160. * @param {MessageEvent} e
  161. * @param {Function} resolve
  162. */
  163. _onPong(e, resolve) {
  164. // 收到pong消息
  165. if (e.data.pong) {
  166. this.connected = true;
  167. this.listeners.forEach(
  168. listener => listener.ping ? this.notListen(listener) : 0
  169. );
  170. console.log("Client: Connected!\n" + new Date());
  171. resolve(this);
  172. }
  173. }
  174. /**
  175. * 向对方发送ping消息
  176. * @returns {Promise<Socket>}
  177. */
  178. _ping() {
  179. return new Promise((resolve, reject) => {
  180. // 绑定pong检查监听器
  181. const listener = this.listen(
  182. e => this._onPong(e, resolve)
  183. );
  184. listener.ping = true;
  185. // 5分钟后超时
  186. setTimeout(
  187. () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
  188. 5 * 60 * 1000
  189. );
  190. // 发送ping消息
  191. this.talk({ ping: true });
  192. });
  193. }
  194. /**
  195. * 检查对方来信是否为ping消息
  196. * @param {MessageEvent} e
  197. * @param {Function} resolve
  198. */
  199. _onPing(e, resolve) {
  200. // 收到ping消息
  201. if (e.data.ping) {
  202. this.target = e.source;
  203. this.connected = true;
  204. this.listeners.forEach(
  205. listener => listener.pong ? this.notListen(listener) : 0
  206. );
  207. console.log("Server: Connected!\n" + new Date());
  208. // resolve 后期约状态无法回退
  209. // 但后续代码仍可执行
  210. resolve(this);
  211. // 回应pong消息
  212. this.talk({ pong: true });
  213. }
  214. }
  215. /**
  216. * 当对方来信是为ping消息时回应pong消息
  217. * @returns {Promise<Socket>}
  218. */
  219. _pong() {
  220. return new Promise(resolve => {
  221. // 绑定ping检查监听器
  222. const listener = this.listen(
  223. e => this._onPing(e, resolve)
  224. );
  225. listener.pong = true;
  226. });
  227. }
  228. /**
  229. * 连接至目标窗口
  230. * @param {boolean} talk_first 是否先发送ping消息
  231. * @param {Window} target 目标窗口
  232. * @returns {Promise<Socket>}
  233. */
  234. connect(talk_first) {
  235. // 先发起握手
  236. if (talk_first) {
  237. return this._ping();
  238. }
  239. // 后发起握手
  240. return this._pong();
  241. }
  242. }
  243. }
  244.  
  245.  
  246. /**
  247. * 域名主函数 ----------------------------------------------------------------------
  248. */
  249.  
  250.  
  251. /**
  252. * chuangyi 主函数
  253. */
  254. async function chuangyi() {
  255. console.log("进入 chuangyi");
  256.  
  257. // 域名级全局常量
  258. const SVG_S = ".seditor-template__path__content svg";
  259.  
  260. while (true) {
  261. if (util.$(SVG_S)) break;
  262. await sleep(SMALL_DELAY);
  263. }
  264. console.log("SVG 出现了,开始执行任务");
  265.  
  266.  
  267. async function main() {
  268. console.log("进入 chuangyi.main");
  269.  
  270. const sock = new util.socket(window.top);
  271. await sock.connect(true);
  272. sock.talk({
  273. action: "copy-svg",
  274. svg: util.svgToStr(util.$(SVG_S))
  275. });
  276. console.log(Date.now());
  277. console.log("SVG 代码已经从 iframe 中发出!");
  278. }
  279.  
  280.  
  281. main();
  282. }
  283.  
  284.  
  285. /**
  286. * iconfont 主函数
  287. */
  288. async function iconfont () {
  289. console.log("进入 iconfont");
  290.  
  291. // 域名级全局常量
  292. const STYLE_TEXT = `
  293. .force-hide {
  294. visibility: hidden !important;
  295. }
  296.  
  297. .block-icon-list li:hover div.icon-cover {
  298. display: grid;
  299. grid-template-columns: auto auto;
  300. }
  301.  
  302. .block-icon-list li .icon-cover span.cover-item-line {
  303. height: auto;
  304. line-height: 50px;
  305. }
  306.  
  307. .svg-copy.disabled:hover {
  308. color: #6d6d6d !important;
  309. }
  310.  
  311. .icon-fuzhidaima:before {
  312. font-size: 24px;
  313. }
  314.  
  315. .copy-icon {
  316. border: none !important;
  317. margin: 0 1.25em !important;
  318. margin: 0 0 0 10px !important;
  319. }
  320.  
  321. .copy-container {
  322. margin: 8px 16px !important;
  323. padding: 0 !important;
  324. font-size: 14px !important;
  325. }
  326. .copy-popup {
  327. top: 60px;
  328. padding: 4px 10px !important;
  329. height: 44px !important;
  330. font-size: 12px !important;
  331. width: fit-content !important;
  332. align-content: center;
  333. 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;
  334. }
  335.  
  336. .swal2-popup {
  337. border-radius:0 !important;
  338. }
  339. `;
  340.  
  341.  
  342. // 域名级全局变量
  343. let sock;
  344.  
  345.  
  346. /**
  347. * 阻塞直到图标区存在
  348. */
  349. while (true) {
  350. if (util.$(".block-icon-list > li")) break;
  351. await sleep(SMALL_DELAY);
  352. }
  353. console.log("图标区出现了,开始执行任务");
  354.  
  355.  
  356. /**
  357. * 使用xhr异步GET请求目标url,返回响应体blob
  358. * @param {string} url
  359. * @returns {Promise<Blob>} blob
  360. */
  361. async function xhrGetBlob(url) {
  362. const xhr = new XMLHttpRequest();
  363. xhr.open("GET", url);
  364. xhr.responseType = "blob";
  365. return new Promise((resolve, reject) => {
  366. xhr.onload = () => {
  367. const code = xhr.status;
  368.  
  369. if (code >= 200 && code <= 299)
  370. resolve(xhr.response);
  371.  
  372. else reject(new Error(`Network Error: ${code}`));
  373. }
  374. xhr.send();
  375. });
  376. }
  377.  
  378.  
  379. /**
  380. * 加载CDN脚本
  381. * @param {string} url
  382. */
  383. async function loadWebScript(url) {
  384. try {
  385. // xhr+eval方式
  386. Function(
  387. await (await xhrGetBlob(url)).text()
  388. )();
  389. } catch(e) {
  390. console.error(e);
  391. // 嵌入<script>方式
  392. const script = document.createElement("script");
  393. script.src = url;
  394. document.body.append(script);
  395. }
  396. }
  397.  
  398.  
  399. function addStyle() {
  400. const id = "iconfont-svg-copy-style";
  401. if (util.$(`#${id}`)) return;
  402.  
  403. const style = document.createElement("style");
  404. style.id = id;
  405. style.innerHTML = STYLE_TEXT;
  406. document.head.append(style);
  407. }
  408.  
  409.  
  410. function showCopySuccess() {
  411. Swal.fire({
  412. text: "复制成功,可以粘贴咯~",
  413. toast: true,
  414. timer: 2000,
  415. showConfirmButton: false,
  416. icon: "success",
  417. position: "top",
  418. customClass: {
  419. popup: "copy-popup",
  420. htmlContainer: "copy-container",
  421. icon: "copy-icon"
  422. }
  423. });
  424. }
  425.  
  426.  
  427. /**
  428. * iframe 返回的 svg string 处理函数
  429. * @param {MessageEvent} e
  430. */
  431. function onPopupDataReceived(e) {
  432. if (e.data.action !== "copy-svg") {
  433. return;
  434. }
  435.  
  436. console.log(Date.now());
  437. console.log("iframe SVG 代码已经收到!");
  438. // 复制 svg string
  439. copyText(e.data.svg);
  440. // 关闭弹窗
  441. util.$(".mp-e2e-dialog-close").click();
  442. // 移除监听器
  443. sock.notListen(onPopupDataReceived);
  444. }
  445.  
  446.  
  447. /**
  448. * 在 iframe 中获取 svg
  449. * @param {HTMLElement} card
  450. */
  451. async function copyInIframe(card) {
  452. // 禁用复制按钮
  453. const icon = util.$.call(card, ".svg-copy");
  454. icon.classList.add("disabled");
  455. icon.removeEventListener("click", copySVGElement, true);
  456. // 触发弹窗
  457. const download = util.$.call(card, "[title='下载']");
  458. download.click();
  459. // 等待弹窗加载完毕
  460. while (true) {
  461. if (util.$("[id*='dlg_']") && util.$("[id*='mask_dlg_']")) {
  462. break;
  463. }
  464. await sleep(SMALL_DELAY);
  465. }
  466. // 隐藏弹窗
  467. const dialogs = await util.$$("[id*='dlg_']");
  468. dialogs.forEach(
  469. elem => elem.classList.add("force-hide")
  470. );
  471.  
  472. let popup;
  473. for (let elem of dialogs) {
  474. if (elem.id.startsWith("dlg_")) {
  475. popup = elem;
  476. }
  477. }
  478. if (!popup) throw new Error("#dlg_ not found");
  479. // 通讯 iframe 等待接收 svg 字符串
  480. const iframes = await util.$$.call(popup, "iframe");
  481. sock = new util.socket(
  482. iframes[0].contentWindow
  483. )
  484. // 当收到 svg string 时复制到剪贴板
  485. sock.listen(onPopupDataReceived);
  486. await sock.connect(false);
  487.  
  488. // 复制完成后再启用复制按钮
  489. await sleep(0);
  490. icon.classList.remove("disabled");
  491. icon.addEventListener("click", copySVGElement, true);
  492. }
  493.  
  494.  
  495. async function copyText(text) {
  496. // 复制到剪贴板
  497. try {
  498. await navigator.clipboard.writeText(text);
  499. } catch (err) {
  500. console.error(err);
  501. console.log(text);
  502. }
  503. // 提示复制成功
  504. showCopySuccess();
  505. }
  506.  
  507.  
  508. /**
  509. * 当点击复制图标时复制 svg 到剪贴板
  510. * @param {PointerEvent} event
  511. */
  512. function copySVGElement(event) {
  513. // 取得svg
  514. const card = event.target.closest("li");
  515. const svg = card.querySelector("svg");
  516.  
  517. // 如果是在 iframe 中的,那就要通过模拟点击下载的方式来获取
  518. if (!svg) {
  519. copyInIframe(card);
  520. return;
  521. }
  522.  
  523. // 序列化
  524. const svg_str = util.svgToStr(svg);
  525. copyText(svg_str);
  526. }
  527.  
  528.  
  529. /**
  530. * 导入 sweet alert 2
  531. * @returns {Promise<void>}
  532. */
  533. function importSweetAlert() {
  534. if (!window.Swal) {
  535. return loadWebScript(
  536. "https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.7.3/sweetalert2.all.min.js"
  537. );
  538. }
  539. return Promise.resolve();
  540. }
  541.  
  542. function addCopyIcon() {
  543. // 获取卡片
  544. const cards = [...document.querySelectorAll(".block-icon-list > li")];
  545. if (!cards[0]) throw new Error("无法选中图标块");
  546. // 制作按钮元素模板
  547. const template = document.createElement("span");
  548. template.title = "复制SVG";
  549. template.classList.add(
  550. "cover-item", "iconfont", "cover-item-line", "icon-fuzhidaima", "svg-copy"
  551. );
  552.  
  553. cards.forEach(card => {
  554. // 添加复制图标
  555. const icon_copy = template.cloneNode();
  556. // 增加复制功能
  557. icon_copy.addEventListener("click", copySVGElement, true);
  558. card.querySelector(".icon-cover").append(icon_copy);
  559. });
  560. }
  561.  
  562.  
  563. async function mainTask() {
  564. console.log("mainTask entered");
  565.  
  566. // 无关路径
  567. const path = location.pathname;
  568. if (!(path.startsWith("/illustrations/") ||
  569. path.startsWith("/collections/")
  570. )) return;
  571. // 等待直到图标块出现
  572. while (true) {
  573. if (util.$(".block-icon-list > li")) break;
  574. await sleep(SMALL_DELAY);
  575. }
  576.  
  577. // 如果已经存在按钮,退出主函数
  578. if (util.$(".icon-cover span.svg-copy")) return;
  579. console.log("正在建造 [复制SVG] 图标...");
  580.  
  581. addStyle();
  582. addCopyIcon();
  583.  
  584. // 导入 sweet alert
  585. importSweetAlert().catch(err => {
  586. console.error(err);
  587. console.log("sweet alert 导入失败,没有提示弹窗了");
  588. });
  589.  
  590. console.log("[复制SVG] 图标 建造完成");
  591. }
  592.  
  593. function delayedTask() {
  594. setTimeout(mainTask, 0);
  595. }
  596.  
  597.  
  598. function getIconsBox() {
  599. const s = ".block-icon-list";
  600. return util.$(`${s} li`).closest(s);
  601. }
  602.  
  603.  
  604. function monitorIconsChanging() {
  605. const observer = new MutationObserver(delayedTask);
  606. observer.observe(
  607. getIconsBox(), { childList: true }
  608. );
  609. }
  610.  
  611.  
  612. const onMainChanged = (function() {
  613. let icons_box = getIconsBox();
  614.  
  615. function inner() {
  616. const new_box = getIconsBox();
  617. if (icons_box === new_box) return;
  618.  
  619. icons_box = new_box;
  620. mainTask();
  621. monitorIconsChanging();
  622. }
  623.  
  624. function delayed() {
  625. setTimeout(inner, LARGE_DELAY);
  626. }
  627.  
  628. return delayed;
  629. })();
  630.  
  631.  
  632. async function monitorMainChanging() {
  633. const elem = (await util.$$("#magix_vf_main"))[0];
  634. const observer = new MutationObserver(onMainChanged);
  635. observer.observe(elem, { attributes: true });
  636. }
  637.  
  638.  
  639. function main() {
  640. console.log("进入 iconfont.main");
  641. mainTask();
  642. monitorMainChanging();
  643. monitorIconsChanging();
  644. window.addEventListener("popstate", mainTask, true);
  645. }
  646.  
  647. main();
  648. }
  649.  
  650.  
  651. /**
  652. * 路由,主函数入口
  653. */
  654. (function route() {
  655. console.log("进入 route");
  656. const host = location.hostname;
  657.  
  658. switch (host) {
  659. case "www.iconfont.cn":
  660. iconfont();
  661. break;
  662. case "make.chuangyi.taobao.com":
  663. chuangyi();
  664. break;
  665.  
  666. default:
  667. console.log(`未知域名,不能处理:${host}`);
  668. break;
  669. }
  670. })();
  671. })();

QingJ © 2025

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