← Back
Editing: 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}; });
Save File
Cancel