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