File: //proc/1/cwd/usr/share/rspamd/www/js/app/icons.js
/*
* Icon rendering for the Rspamd WebUI.
*
* Replaces <i class="fas fa-<name>"> placeholders with inline
* <svg class="svg-inline--fa"><use href="img/icons.svg#fa-<name>"/></svg>,
* sourcing glyphs from the prebuilt Font Awesome subset sprite
* (interface/img/icons.svg, generated by icons-build.mjs at the repo root).
* This replaces the Font Awesome "SVG with JS" framework (~900 KB of JS/CSS).
*
* Loaded for its side effect: an initial sweep plus a MutationObserver that
* renders icons inserted later (lazy-loaded tabs, table rows). Exports
* setIcon() for code that swaps an icon dynamically (theme toggle, map modal).
*/
define([], () => {
"use strict";
const SPRITE = "img/icons.svg";
const NS = "http://www.w3.org/2000/svg";
const ICON_CLASS = /^fa-[a-z0-9-]+$/u;
const STYLE_CLASSES = new Set(["fas", "fa-solid", "fa-regular", "fa-brands"]);
const UTILITIES = new Set([
"fa-border",
"fa-bounce",
"fa-fade",
"fa-flip",
"fa-fw",
"fa-lg",
"fa-pull-left",
"fa-pull-right",
"fa-pulse",
"fa-sm",
"fa-spin",
"fa-xs",
]);
// Whether `cls` is an icon-name class (fa-<name>): not a style prefix
// (fas, fa-solid, ...) and not a modifier/utility (fa-spin, fa-fw, ...).
function isIconName(cls) {
return ICON_CLASS.test(cls) && !STYLE_CLASSES.has(cls) && !UTILITIES.has(cls);
}
// Whether `cls` should be carried from the <i> to the rendered <svg>:
// keep spacing and modifier classes (fa-spin, ...); drop style prefixes,
// the icon-name class, and svg-inline--fa.
function keepClass(cls) {
return cls !== "svg-inline--fa" && !STYLE_CLASSES.has(cls) && !isIconName(cls);
}
function iconName(classList) {
return Array.from(classList).find(isIconName);
}
// Build an <svg> referencing glyph `name` in the sprite, carrying `classes`.
function buildSvg(name, classes) {
const svg = document.createElementNS(NS, "svg");
svg.classList.add("svg-inline--fa");
svg.setAttribute("data-icon", name);
classes.forEach((cls) => svg.classList.add(cls));
const use = document.createElementNS(NS, "use");
use.setAttribute("href", `${SPRITE}#${name}`);
svg.append(use);
return svg;
}
// Replace `el` (an <i> placeholder or an already-rendered <svg>) with an
// <svg> showing glyph `name`, preserving non-class attributes (id, title,
// aria-*, ...) and modifier classes (spacing, fa-spin, ...).
function setIcon(el, name) {
const keep = Array.from(el.classList).filter(keepClass);
const svg = buildSvg(name, keep);
Array.from(el.attributes).forEach((attr) => {
if (attr.name !== "class" && attr.name !== "data-icon") {
svg.setAttribute(attr.name, attr.value);
}
});
el.replaceWith(svg);
return svg;
}
function findIcons(root) {
if (root.matches && root.matches("i.fas, i.fa-solid")) {
return [root];
}
return Array.from(root.querySelectorAll("i.fas, i.fa-solid"));
}
function render(root) {
if (!root || root.nodeType !== Node.ELEMENT_NODE) {
return;
}
findIcons(root).forEach((el) => {
const name = iconName(el.classList);
if (name) {
setIcon(el, name);
}
});
}
// Initial sweep, then watch nodes added later (lazy tabs, table rows).
render(document.body);
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => render(node));
});
}).observe(document.body, {childList: true, subtree: true});
return {setIcon};
});