From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:40:42 +0000 (+0300) Subject: [Rework] WebUI: Replace Glyphicons with FontAwesome SVG icons X-Git-Tag: 3.14.0~35^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=073fdf986d651d64f53ce069815eabdee199b4e8;p=thirdparty%2Frspamd.git [Rework] WebUI: Replace Glyphicons with FontAwesome SVG icons The implementation uses a global MutationObserver watching document.body that: - Detects when new fooicon elements are added to the DOM - Detects when class attributes change on existing fooicon elements (e.g., sort icon cycling) - Automatically processes and replaces icons Benefits: ✓ Sharp SVG rendering at all zoom levels (vs blurry webfonts) ✓ No font loading issues or CORS problems ✓ Reduced code footprint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: moisseev <2275981+moisseev@users.noreply.github.com> --- diff --git a/interface/css/FooTable.FontAwesome.css b/interface/css/FooTable.FontAwesome.css new file mode 100644 index 0000000000..aacebfdb6a --- /dev/null +++ b/interface/css/FooTable.FontAwesome.css @@ -0,0 +1,28 @@ +/* Hide the old Glyphicons pseudo-elements */ +.fooicon::before, +.fooicon::after { + content: none !important; + display: none; +} + +/* Critical: allow clicks to pass through SVG to parent span */ +.fooicon svg { + pointer-events: none; +} + +/* Smaller toggle icon and icons in footable header */ +.footable-header .fooicon svg, +.footable-toggle.fooicon svg { + font-size: 12px; +} + +/* Stop span rotation for loader - we animate the SVG instead */ +.fooicon-loader { + animation: none !important; + transform: none !important; +} + +/* Make spinner icon larger to match original size */ +.fooicon-loader.fooicon svg { + font-size: 36px; +} diff --git a/interface/css/FooTable.Glyphicons.css b/interface/css/FooTable.Glyphicons.css deleted file mode 100644 index c68f66eaa7..0000000000 --- a/interface/css/FooTable.Glyphicons.css +++ /dev/null @@ -1,62 +0,0 @@ -/* Glyphicons Icons - We're not actually using Glyphicons classes but instead provide a simple mapping - from Glyphicons to FooTable class names. */ -.fooicon { - position: relative; - top: 1px; - display: inline-block; - /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ - font-family: "Glyphicons Halflings" !important; - font-style: normal; - font-weight: 400; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.fooicon::before, -.fooicon::after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.fooicon-loader::before { - content: "\e030"; -} -.fooicon-plus::before { - content: "\2b"; -} -.fooicon-minus::before { - content: "\2212"; -} -.fooicon-search::before { - content: "\e003"; -} -.fooicon-remove::before { - content: "\e014"; -} -.fooicon-sort::before { - content: "\e150"; -} -.fooicon-sort-asc::before { - content: "\e155"; -} -.fooicon-sort-desc::before { - content: "\e156"; -} -.fooicon-pencil::before { - content: "\270f"; -} -.fooicon-trash::before { - content: "\e020"; -} -.fooicon-eye-close::before { - content: "\e106"; -} -.fooicon-flash::before { - content: "\e162"; -} -.fooicon-cog::before { - content: "\e019"; -} -.fooicon-stats::before { - content: "\e185"; -} diff --git a/interface/css/font-glyphicons.css b/interface/css/font-glyphicons.css deleted file mode 100644 index ecb237bff2..0000000000 --- a/interface/css/font-glyphicons.css +++ /dev/null @@ -1,805 +0,0 @@ -/* https://gist.github.com/itchief/e3aa0888c959ee367cd69f5d83ff06f9 */ - -@font-face { - font-family: "Glyphicons Halflings"; - src: url("../fonts/glyphicons-halflings-regular.eot"); - src: url("../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"), url("../fonts/glyphicons-halflings-regular.woff") format("woff"), url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg"); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: "Glyphicons Halflings"; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ - font-style: normal; - font-weight: 400; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk::before { - content: "\002a"; -} -.glyphicon-plus::before { - content: "\002b"; -} -.glyphicon-euro::before, -.glyphicon-eur::before { - content: "\20ac"; -} -.glyphicon-minus::before { - content: "\2212"; -} -.glyphicon-cloud::before { - content: "\2601"; -} -.glyphicon-envelope::before { - content: "\2709"; -} -.glyphicon-pencil::before { - content: "\270f"; -} -.glyphicon-glass::before { - content: "\e001"; -} -.glyphicon-music::before { - content: "\e002"; -} -.glyphicon-search::before { - content: "\e003"; -} -.glyphicon-heart::before { - content: "\e005"; -} -.glyphicon-star::before { - content: "\e006"; -} -.glyphicon-star-empty::before { - content: "\e007"; -} -.glyphicon-user::before { - content: "\e008"; -} -.glyphicon-film::before { - content: "\e009"; -} -.glyphicon-th-large::before { - content: "\e010"; -} -.glyphicon-th::before { - content: "\e011"; -} -.glyphicon-th-list::before { - content: "\e012"; -} -.glyphicon-ok::before { - content: "\e013"; -} -.glyphicon-remove::before { - content: "\e014"; -} -.glyphicon-zoom-in::before { - content: "\e015"; -} -.glyphicon-zoom-out::before { - content: "\e016"; -} -.glyphicon-off::before { - content: "\e017"; -} -.glyphicon-signal::before { - content: "\e018"; -} -.glyphicon-cog::before { - content: "\e019"; -} -.glyphicon-trash::before { - content: "\e020"; -} -.glyphicon-home::before { - content: "\e021"; -} -.glyphicon-file::before { - content: "\e022"; -} -.glyphicon-time::before { - content: "\e023"; -} -.glyphicon-road::before { - content: "\e024"; -} -.glyphicon-download-alt::before { - content: "\e025"; -} -.glyphicon-download::before { - content: "\e026"; -} -.glyphicon-upload::before { - content: "\e027"; -} -.glyphicon-inbox::before { - content: "\e028"; -} -.glyphicon-play-circle::before { - content: "\e029"; -} -.glyphicon-repeat::before { - content: "\e030"; -} -.glyphicon-refresh::before { - content: "\e031"; -} -.glyphicon-list-alt::before { - content: "\e032"; -} -.glyphicon-lock::before { - content: "\e033"; -} -.glyphicon-flag::before { - content: "\e034"; -} -.glyphicon-headphones::before { - content: "\e035"; -} -.glyphicon-volume-off::before { - content: "\e036"; -} -.glyphicon-volume-down::before { - content: "\e037"; -} -.glyphicon-volume-up::before { - content: "\e038"; -} -.glyphicon-qrcode::before { - content: "\e039"; -} -.glyphicon-barcode::before { - content: "\e040"; -} -.glyphicon-tag::before { - content: "\e041"; -} -.glyphicon-tags::before { - content: "\e042"; -} -.glyphicon-book::before { - content: "\e043"; -} -.glyphicon-bookmark::before { - content: "\e044"; -} -.glyphicon-print::before { - content: "\e045"; -} -.glyphicon-camera::before { - content: "\e046"; -} -.glyphicon-font::before { - content: "\e047"; -} -.glyphicon-bold::before { - content: "\e048"; -} -.glyphicon-italic::before { - content: "\e049"; -} -.glyphicon-text-height::before { - content: "\e050"; -} -.glyphicon-text-width::before { - content: "\e051"; -} -.glyphicon-align-left::before { - content: "\e052"; -} -.glyphicon-align-center::before { - content: "\e053"; -} -.glyphicon-align-right::before { - content: "\e054"; -} -.glyphicon-align-justify::before { - content: "\e055"; -} -.glyphicon-list::before { - content: "\e056"; -} -.glyphicon-indent-left::before { - content: "\e057"; -} -.glyphicon-indent-right::before { - content: "\e058"; -} -.glyphicon-facetime-video::before { - content: "\e059"; -} -.glyphicon-picture::before { - content: "\e060"; -} -.glyphicon-map-marker::before { - content: "\e062"; -} -.glyphicon-adjust::before { - content: "\e063"; -} -.glyphicon-tint::before { - content: "\e064"; -} -.glyphicon-edit::before { - content: "\e065"; -} -.glyphicon-share::before { - content: "\e066"; -} -.glyphicon-check::before { - content: "\e067"; -} -.glyphicon-move::before { - content: "\e068"; -} -.glyphicon-step-backward::before { - content: "\e069"; -} -.glyphicon-fast-backward::before { - content: "\e070"; -} -.glyphicon-backward::before { - content: "\e071"; -} -.glyphicon-play::before { - content: "\e072"; -} -.glyphicon-pause::before { - content: "\e073"; -} -.glyphicon-stop::before { - content: "\e074"; -} -.glyphicon-forward::before { - content: "\e075"; -} -.glyphicon-fast-forward::before { - content: "\e076"; -} -.glyphicon-step-forward::before { - content: "\e077"; -} -.glyphicon-eject::before { - content: "\e078"; -} -.glyphicon-chevron-left::before { - content: "\e079"; -} -.glyphicon-chevron-right::before { - content: "\e080"; -} -.glyphicon-plus-sign::before { - content: "\e081"; -} -.glyphicon-minus-sign::before { - content: "\e082"; -} -.glyphicon-remove-sign::before { - content: "\e083"; -} -.glyphicon-ok-sign::before { - content: "\e084"; -} -.glyphicon-question-sign::before { - content: "\e085"; -} -.glyphicon-info-sign::before { - content: "\e086"; -} -.glyphicon-screenshot::before { - content: "\e087"; -} -.glyphicon-remove-circle::before { - content: "\e088"; -} -.glyphicon-ok-circle::before { - content: "\e089"; -} -.glyphicon-ban-circle::before { - content: "\e090"; -} -.glyphicon-arrow-left::before { - content: "\e091"; -} -.glyphicon-arrow-right::before { - content: "\e092"; -} -.glyphicon-arrow-up::before { - content: "\e093"; -} -.glyphicon-arrow-down::before { - content: "\e094"; -} -.glyphicon-share-alt::before { - content: "\e095"; -} -.glyphicon-resize-full::before { - content: "\e096"; -} -.glyphicon-resize-small::before { - content: "\e097"; -} -.glyphicon-exclamation-sign::before { - content: "\e101"; -} -.glyphicon-gift::before { - content: "\e102"; -} -.glyphicon-leaf::before { - content: "\e103"; -} -.glyphicon-fire::before { - content: "\e104"; -} -.glyphicon-eye-open::before { - content: "\e105"; -} -.glyphicon-eye-close::before { - content: "\e106"; -} -.glyphicon-warning-sign::before { - content: "\e107"; -} -.glyphicon-plane::before { - content: "\e108"; -} -.glyphicon-calendar::before { - content: "\e109"; -} -.glyphicon-random::before { - content: "\e110"; -} -.glyphicon-comment::before { - content: "\e111"; -} -.glyphicon-magnet::before { - content: "\e112"; -} -.glyphicon-chevron-up::before { - content: "\e113"; -} -.glyphicon-chevron-down::before { - content: "\e114"; -} -.glyphicon-retweet::before { - content: "\e115"; -} -.glyphicon-shopping-cart::before { - content: "\e116"; -} -.glyphicon-folder-close::before { - content: "\e117"; -} -.glyphicon-folder-open::before { - content: "\e118"; -} -.glyphicon-resize-vertical::before { - content: "\e119"; -} -.glyphicon-resize-horizontal::before { - content: "\e120"; -} -.glyphicon-hdd::before { - content: "\e121"; -} -.glyphicon-bullhorn::before { - content: "\e122"; -} -.glyphicon-bell::before { - content: "\e123"; -} -.glyphicon-certificate::before { - content: "\e124"; -} -.glyphicon-thumbs-up::before { - content: "\e125"; -} -.glyphicon-thumbs-down::before { - content: "\e126"; -} -.glyphicon-hand-right::before { - content: "\e127"; -} -.glyphicon-hand-left::before { - content: "\e128"; -} -.glyphicon-hand-up::before { - content: "\e129"; -} -.glyphicon-hand-down::before { - content: "\e130"; -} -.glyphicon-circle-arrow-right::before { - content: "\e131"; -} -.glyphicon-circle-arrow-left::before { - content: "\e132"; -} -.glyphicon-circle-arrow-up::before { - content: "\e133"; -} -.glyphicon-circle-arrow-down::before { - content: "\e134"; -} -.glyphicon-globe::before { - content: "\e135"; -} -.glyphicon-wrench::before { - content: "\e136"; -} -.glyphicon-tasks::before { - content: "\e137"; -} -.glyphicon-filter::before { - content: "\e138"; -} -.glyphicon-briefcase::before { - content: "\e139"; -} -.glyphicon-fullscreen::before { - content: "\e140"; -} -.glyphicon-dashboard::before { - content: "\e141"; -} -.glyphicon-paperclip::before { - content: "\e142"; -} -.glyphicon-heart-empty::before { - content: "\e143"; -} -.glyphicon-link::before { - content: "\e144"; -} -.glyphicon-phone::before { - content: "\e145"; -} -.glyphicon-pushpin::before { - content: "\e146"; -} -.glyphicon-usd::before { - content: "\e148"; -} -.glyphicon-gbp::before { - content: "\e149"; -} -.glyphicon-sort::before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet::before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt::before { - content: "\e152"; -} -.glyphicon-sort-by-order::before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt::before { - content: "\e154"; -} -.glyphicon-sort-by-attributes::before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt::before { - content: "\e156"; -} -.glyphicon-unchecked::before { - content: "\e157"; -} -.glyphicon-expand::before { - content: "\e158"; -} -.glyphicon-collapse-down::before { - content: "\e159"; -} -.glyphicon-collapse-up::before { - content: "\e160"; -} -.glyphicon-log-in::before { - content: "\e161"; -} -.glyphicon-flash::before { - content: "\e162"; -} -.glyphicon-log-out::before { - content: "\e163"; -} -.glyphicon-new-window::before { - content: "\e164"; -} -.glyphicon-record::before { - content: "\e165"; -} -.glyphicon-save::before { - content: "\e166"; -} -.glyphicon-open::before { - content: "\e167"; -} -.glyphicon-saved::before { - content: "\e168"; -} -.glyphicon-import::before { - content: "\e169"; -} -.glyphicon-export::before { - content: "\e170"; -} -.glyphicon-send::before { - content: "\e171"; -} -.glyphicon-floppy-disk::before { - content: "\e172"; -} -.glyphicon-floppy-saved::before { - content: "\e173"; -} -.glyphicon-floppy-remove::before { - content: "\e174"; -} -.glyphicon-floppy-save::before { - content: "\e175"; -} -.glyphicon-floppy-open::before { - content: "\e176"; -} -.glyphicon-credit-card::before { - content: "\e177"; -} -.glyphicon-transfer::before { - content: "\e178"; -} -.glyphicon-cutlery::before { - content: "\e179"; -} -.glyphicon-header::before { - content: "\e180"; -} -.glyphicon-compressed::before { - content: "\e181"; -} -.glyphicon-earphone::before { - content: "\e182"; -} -.glyphicon-phone-alt::before { - content: "\e183"; -} -.glyphicon-tower::before { - content: "\e184"; -} -.glyphicon-stats::before { - content: "\e185"; -} -.glyphicon-sd-video::before { - content: "\e186"; -} -.glyphicon-hd-video::before { - content: "\e187"; -} -.glyphicon-subtitles::before { - content: "\e188"; -} -.glyphicon-sound-stereo::before { - content: "\e189"; -} -.glyphicon-sound-dolby::before { - content: "\e190"; -} -.glyphicon-sound-5-1::before { - content: "\e191"; -} -.glyphicon-sound-6-1::before { - content: "\e192"; -} -.glyphicon-sound-7-1::before { - content: "\e193"; -} -.glyphicon-copyright-mark::before { - content: "\e194"; -} -.glyphicon-registration-mark::before { - content: "\e195"; -} -.glyphicon-cloud-download::before { - content: "\e197"; -} -.glyphicon-cloud-upload::before { - content: "\e198"; -} -.glyphicon-tree-conifer::before { - content: "\e199"; -} -.glyphicon-tree-deciduous::before { - content: "\e200"; -} -.glyphicon-cd::before { - content: "\e201"; -} -.glyphicon-save-file::before { - content: "\e202"; -} -.glyphicon-open-file::before { - content: "\e203"; -} -.glyphicon-level-up::before { - content: "\e204"; -} -.glyphicon-copy::before { - content: "\e205"; -} -.glyphicon-paste::before { - content: "\e206"; -} -.glyphicon-alert::before { - content: "\e209"; -} -.glyphicon-equalizer::before { - content: "\e210"; -} -.glyphicon-king::before { - content: "\e211"; -} -.glyphicon-queen::before { - content: "\e212"; -} -.glyphicon-pawn::before { - content: "\e213"; -} -.glyphicon-bishop::before { - content: "\e214"; -} -.glyphicon-knight::before { - content: "\e215"; -} -.glyphicon-baby-formula::before { - content: "\e216"; -} -.glyphicon-tent::before { - content: "\26fa"; -} -.glyphicon-blackboard::before { - content: "\e218"; -} -.glyphicon-bed::before { - content: "\e219"; -} -.glyphicon-apple::before { - content: "\f8ff"; -} -.glyphicon-erase::before { - content: "\e221"; -} -.glyphicon-hourglass::before { - content: "\231b"; -} -.glyphicon-lamp::before { - content: "\e223"; -} -.glyphicon-duplicate::before { - content: "\e224"; -} -.glyphicon-piggy-bank::before { - content: "\e225"; -} -.glyphicon-scissors::before { - content: "\e226"; -} -.glyphicon-bitcoin::before { - content: "\e227"; -} -.glyphicon-btc::before { - content: "\e227"; -} -.glyphicon-xbt::before { - content: "\e227"; -} -.glyphicon-yen::before { - content: "\00a5"; -} -.glyphicon-jpy::before { - content: "\00a5"; -} -.glyphicon-ruble::before { - content: "\20bd"; -} -.glyphicon-rub::before { - content: "\20bd"; -} -.glyphicon-scale::before { - content: "\e230"; -} -.glyphicon-ice-lolly::before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted::before { - content: "\e232"; -} -.glyphicon-education::before { - content: "\e233"; -} -.glyphicon-option-horizontal::before { - content: "\e234"; -} -.glyphicon-option-vertical::before { - content: "\e235"; -} -.glyphicon-menu-hamburger::before { - content: "\e236"; -} -.glyphicon-modal-window::before { - content: "\e237"; -} -.glyphicon-oil::before { - content: "\e238"; -} -.glyphicon-grain::before { - content: "\e239"; -} -.glyphicon-sunglasses::before { - content: "\e240"; -} -.glyphicon-text-size::before { - content: "\e241"; -} -.glyphicon-text-color::before { - content: "\e242"; -} -.glyphicon-text-background::before { - content: "\e243"; -} -.glyphicon-object-align-top::before { - content: "\e244"; -} -.glyphicon-object-align-bottom::before { - content: "\e245"; -} -.glyphicon-object-align-horizontal::before { - content: "\e246"; -} -.glyphicon-object-align-left::before { - content: "\e247"; -} -.glyphicon-object-align-vertical::before { - content: "\e248"; -} -.glyphicon-object-align-right::before { - content: "\e249"; -} -.glyphicon-triangle-right::before { - content: "\e250"; -} -.glyphicon-triangle-left::before { - content: "\e251"; -} -.glyphicon-triangle-bottom::before { - content: "\e252"; -} -.glyphicon-triangle-top::before { - content: "\e253"; -} -.glyphicon-console::before { - content: "\e254"; -} -.glyphicon-superscript::before { - content: "\e255"; -} -.glyphicon-subscript::before { - content: "\e256"; -} -.glyphicon-menu-left::before { - content: "\e257"; -} -.glyphicon-menu-right::before { - content: "\e258"; -} -.glyphicon-menu-down::before { - content: "\e259"; -} -.glyphicon-menu-up::before { - content: "\e260"; -} diff --git a/interface/fonts/glyphicons-halflings-regular.ttf b/interface/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609a..0000000000 Binary files a/interface/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/interface/fonts/glyphicons-halflings-regular.woff b/interface/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e612858f8..0000000000 Binary files a/interface/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/interface/fonts/glyphicons-halflings-regular.woff2 b/interface/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c3..0000000000 Binary files a/interface/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/interface/index.html b/interface/index.html index 1cd49203e1..c50d17fc63 100644 --- a/interface/index.html +++ b/interface/index.html @@ -15,9 +15,8 @@ - - + diff --git a/interface/js/app/footable-fontawesome.js b/interface/js/app/footable-fontawesome.js new file mode 100644 index 0000000000..109409f462 --- /dev/null +++ b/interface/js/app/footable-fontawesome.js @@ -0,0 +1,156 @@ +/** + * Replace FooTable fooicon elements with FontAwesome SVG icons + */ +define(["jquery", "fontawesome"], ($, FontAwesome) => { + "use strict"; + + // Icon mapping from FooTable classes to FontAwesome icon definitions + // Each entry: [prefix, iconName, additionalClasses (optional, space-separated string)] + const iconMap = { + "fooicon-loader": ["fas", "spinner", "fa-spin"], + "fooicon-plus": ["fas", "plus"], + "fooicon-minus": ["fas", "minus"], + "fooicon-search": ["fas", "search"], + "fooicon-remove": ["fas", "times"], + "fooicon-sort": ["fas", "arrows-up-down"], + "fooicon-sort-asc": ["fas", "arrow-down-short-wide"], + "fooicon-sort-desc": ["fas", "arrow-up-wide-short"], + }; + + let observer = null; + + /** + * Process a single fooicon element and replace with SVG + */ + function processIcon(element) { + const $el = $(element); + + // Find which fooicon-* class this element has + const classList = element.className.split(/\s+/); + const fooClass = classList.find((cls) => cls.startsWith("fooicon-") && iconMap[cls]); + + if (!fooClass) return; + + // Check if already processed with this icon + const currentIcon = $el.data("fa-current-icon"); + if (currentIcon === fooClass) return; + + const iconDef = iconMap[fooClass]; + const [prefix, iconName, additionalClasses] = iconDef; + + try { + // Create FontAwesome SVG icon + const iconObj = FontAwesome.icon({prefix, iconName}); + + // Create jQuery object for SVG + const $svg = $(iconObj.node[0]); + + // Ensure clicks pass through SVG to parent span + $svg.css("pointer-events", "none"); + + // Add optional additional classes (e.g., fa-spin) + if (additionalClasses) { + $svg.addClass(additionalClasses); + } + + // Replace element content with SVG + $el.empty().append($svg); + + // Mark as processed with current icon type + $el.data("fa-current-icon", fooClass); + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Failed to create FontAwesome icon for ${fooClass}:`, e); + } + } + + /** + * Process all existing fooicon elements in the document + */ + function processAllIcons() { + document.querySelectorAll(".fooicon[class*='fooicon-']").forEach(processIcon); + } + + /** + * Initialize the MutationObserver to watch for icon changes + */ + function initObserver() { + if (observer) return; // Already initialized + + observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Handle added nodes + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if the node itself is a fooicon + if (node.classList && node.classList.contains("fooicon")) { + processIcon(node); + } + // Check for fooicon descendants + if (node.querySelectorAll) { + node.querySelectorAll(".fooicon[class*='fooicon-']").forEach(processIcon); + } + } + }); + } + + // Handle class attribute changes (e.g., sort icon toggling) + if (mutation.type === "attributes" && mutation.attributeName === "class") { + const {target} = mutation; + if (target.classList && target.classList.contains("fooicon")) { + processIcon(target); + } + } + }); + }); + + // Observe the entire document for maximum coverage + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class"] + }); + } + + /** + * Public API + */ + + return { + + /** + * Initialize icon replacement globally + * Should be called once when the app starts + */ + init: () => { + // Process any existing icons + processAllIcons(); + + // Start observing for future changes + initObserver(); + }, + + /** + * Manually process icons in a specific container + * Useful for immediate processing without waiting for observer + */ + process: (container) => { + const $container = (typeof container === "string") ? $(container) : container; + $container.find(".fooicon[class*='fooicon-']").each(function () { + processIcon(this); + }); + }, + + /** + * Destroy the observer (cleanup) + */ + destroy: () => { + if (observer) { + observer.disconnect(); + observer = null; + } + } + }; +}); diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js index 0434549ef1..ca3319f285 100644 --- a/interface/js/app/rspamd.js +++ b/interface/js/app/rspamd.js @@ -254,6 +254,11 @@ define(["jquery", "app/common", "stickytabs", "visibility", $("#preloader").addClass("d-none"); $("#navBar, #mainUI").removeClass("d-none"); $(".nav-tabs-sticky").stickyTabs({initialTab: "#status_nav"}); + + // Initialize FontAwesome icon replacement for FooTable + require(["app/footable-fontawesome"], (FootableFA) => { + FootableFA.init(); + }); }, errorMessage: "Cannot get server status", server: "All SERVERS" diff --git a/interface/js/main.js b/interface/js/main.js index 0e226a0b5e..4cf97ae4c7 100644 --- a/interface/js/main.js +++ b/interface/js/main.js @@ -55,7 +55,7 @@ requirejs.onError = function (e) { "" + ""; throw e;