]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Rework] WebUI: remove jQuery from leaf modules
authorAlexander Moisseev <moiseev@mezonplus.ru>
Sun, 5 Jul 2026 12:12:52 +0000 (15:12 +0300)
committerAlexander Moisseev <moiseev@mezonplus.ru>
Sun, 5 Jul 2026 17:21:16 +0000 (20:21 +0300)
Migrate graph.js, selectors.js, history.js, and symbols.js off
jQuery: $.extend/$.each/$.trim and $().val/prop/attr/hasClass/is/
addClass/removeClass/change/click/on to native DOM and the shared
common.el / common.delegate helpers added in the common.js stage.

Delegated handlers on Tabulator-rendered DOM (.scorebar) move to
common.delegate; the repeated button disable/enable patterns
collapse into small per-module helpers.

jQuery remains loaded for the remaining modules (config, libft,
stats, upload, rspamd) — removed in later stages.

interface/js/app/graph.js
interface/js/app/history.js
interface/js/app/selectors.js
interface/js/app/symbols.js

index e99654815651a8a3c4951966b410c1d9b0e361ba..c84fa9171a6c8c90534510335e97c1515cb8502d 100644 (file)
@@ -3,9 +3,8 @@
  * Copyright (C) 2017 Alexander Moisseev
  */
 
-define(["jquery", "app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "tabulator"],
-    // eslint-disable-next-line max-params -- AMD factory: each dep needs a param
-    ($, common, tabUtils, D3Evolution, D3Pie, d3, Tabulator) => {
+define(["app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "tabulator"],
+    (common, tabUtils, D3Evolution, D3Pie, d3, Tabulator) => {
         "use strict";
 
         const rrd_pie_config = {
@@ -59,23 +58,24 @@ define(["jquery", "app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "
             };
 
             function initGraph() {
-                const graph = new D3Evolution("graph", $.extend({}, graph_options, {
+                const graph = new D3Evolution("graph", {
+                    ...graph_options,
                     yScale: common.getSelector("selYScale"),
                     type: common.getSelector("selType"),
                     interpolate: common.getSelector("selInterpolate"),
                     convert: common.getSelector("selConvert"),
-                }));
-                $("#selYScale").change(function () {
-                    graph.yScale(this.value);
                 });
-                $("#selConvert").change(function () {
-                    graph.convert(this.value);
+                document.getElementById("selYScale").addEventListener("change", (e) => {
+                    graph.yScale(e.currentTarget.value);
                 });
-                $("#selType").change(function () {
-                    graph.type(this.value);
+                document.getElementById("selConvert").addEventListener("change", (e) => {
+                    graph.convert(e.currentTarget.value);
                 });
-                $("#selInterpolate").change(function () {
-                    graph.interpolate(this.value);
+                document.getElementById("selType").addEventListener("change", (e) => {
+                    graph.type(e.currentTarget.value);
+                });
+                document.getElementById("selInterpolate").addEventListener("change", (e) => {
+                    graph.interpolate(e.currentTarget.value);
                 });
 
                 return graph;
@@ -177,7 +177,9 @@ define(["jquery", "app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "
                 graphs.graph.data(data);
                 if (unit !== prevUnit) {
                     graphs.graph.yAxisLabel("Message rate, " + unit);
-                    $(".unit").text(unit);
+                    document.querySelectorAll(".unit").forEach((el) => {
+                        el.textContent = unit;
+                    });
                     prevUnit = unit;
                 }
                 drawRrdTable(rrd_summary.rows, unit);
@@ -225,7 +227,11 @@ define(["jquery", "app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "
                     }
                     updateWidgets(data);
                 },
-                complete: function () { $("#refresh").removeAttr("disabled").removeClass("disabled"); },
+                complete: function () {
+                    const refreshBtn = document.getElementById("refresh");
+                    refreshBtn.disabled = false;
+                    refreshBtn.classList.remove("disabled");
+                },
                 errorMessage: "Cannot receive throughput data",
                 errorOnceId: "alerted_graph_",
                 data: {type: type}
@@ -234,13 +240,17 @@ define(["jquery", "app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "
 
 
         // Handling mouse events on overlapping elements
-        $("#rrd-pie").mouseover(() => {
-            $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "200");
-            $("#rrd-table_toggle").css("z-index", "300");
+        function setZIndex(selector, value) {
+            document.querySelectorAll(selector).forEach((el) => {
+                el.style.zIndex = value;
+            });
+        }
+        document.getElementById("rrd-pie").addEventListener("mouseover", () => {
+            setZIndex("#rrd-pie, #rrd-pie-tooltip", "200");
+            setZIndex("#rrd-table_toggle", "300");
         });
-        $("#rrd-table_toggle").mouseover(() => {
-            $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "0");
-            $("#rrd-table_toggle").css("z-index", "0");
+        document.getElementById("rrd-table_toggle").addEventListener("mouseover", () => {
+            setZIndex("#rrd-pie, #rrd-pie-tooltip, #rrd-table_toggle", "0");
         });
 
         return ui;
index aa886cc4d801cb73f2455c4ac67a57a4f96a6cc3..c5b90519ddd727e1203fd21ee621266849194931 100644 (file)
@@ -2,8 +2,8 @@
  * Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
  */
 
-define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
-    ($, common, libft, tabUtils, Tabulator) => {
+define(["app/common", "app/libft", "app/tab-utils", "tabulator"],
+    (common, libft, tabUtils, Tabulator) => {
         "use strict";
         const ui = {};
         let prevVersion = null;
@@ -21,7 +21,7 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
 
             common.hide("#selSymOrder_history, label[for='selSymOrder_history']");
 
-            $.each(data, (i, item) => {
+            data.forEach((item) => {
                 libft.preprocess_item(item);
                 item.symbols = Object.keys(item.symbols)
                     .map((key) => item.symbols[key])
@@ -115,6 +115,16 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
             legacy: columns_legacy()
         };
 
+        // Toggle the Refresh and Update-history buttons together: the `disabled`
+        // property controls behaviour, the `disabled` class keeps Bootstrap
+        // `.btn` styling in sync (redundant for the attribute alone on buttons).
+        function setControlsDisabled(disabled) {
+            document.querySelectorAll("#refresh, #updateHistory").forEach((el) => {
+                el.disabled = disabled;
+                el.classList.toggle("disabled", disabled);
+            });
+        }
+
         function process_history_data(data) {
             const process_functions = {
                 2: libft.process_history_v2,
@@ -146,7 +156,7 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
         }
 
         ui.getHistory = function () {
-            $("#refresh, #updateHistory").attr("disabled", true);
+            setControlsDisabled(true);
             const histTo = histFrom - 1 + histCount;
             common.query(`history?from=${histFrom}&to=${histTo}`, {
                 success: function (req_data) {
@@ -191,7 +201,10 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
                             libft.destroyTable("history");
                             libft.initHistoryTable(data, items, "history", get_history_columns(data), false,
                                 () => {
-                                    $("#history .tab-columns-dropdown .btn-dropdown-apply").removeAttr("disabled");
+                                    document.querySelectorAll("#history .tab-columns-dropdown .btn-dropdown-apply")
+                                        .forEach((el) => {
+                                            el.disabled = false;
+                                        });
                                     ui.updateHistoryControlsState();
                                     if (version) libft.bindFuzzyHashButtons("history");
                                 });
@@ -288,7 +301,7 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
                         .filter((d) => d.status) // filter out unavailable neighbours
                         .map((d) => d.data);
                     const rows = [].concat.apply([], neighbours_data);
-                    $.each(rows, (i, item) => {
+                    rows.forEach((item) => {
                         for (const prop in item) {
                             if (!{}.hasOwnProperty.call(item, prop)) continue;
                             if (typeof item[prop] === "string") item[prop] = common.escapeHTML(item[prop]);
@@ -301,24 +314,13 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
                     }
                 }
             });
-
-            $("#updateErrors").off("click");
-            $("#updateErrors").on("click", (e) => {
-                e.preventDefault();
-                ui.getErrors();
-            });
         };
 
         ui.updateHistoryControlsState = function () {
-            const from = parseInt($("#history-from").val(), 10);
-            const count = parseInt($("#history-count").val(), 10);
+            const from = parseInt(document.getElementById("history-from").value, 10);
+            const count = parseInt(document.getElementById("history-count").value, 10);
             const valid = !(isNaN(from) || from < 0 || isNaN(count) || count < 1);
-
-            if (valid) {
-                $("#refresh, #updateHistory").removeAttr("disabled").removeClass("disabled");
-            } else {
-                $("#refresh, #updateHistory").attr("disabled", true).addClass("disabled");
-            }
+            setControlsDisabled(!valid);
         };
 
         function validateAndClampInput(el) {
@@ -326,42 +328,42 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
             let v = parseInt(el.value, 10);
             if (isNaN(v) || v < min) {
                 v = min;
-                $(el).addClass("is-invalid");
+                el.classList.add("is-invalid");
             } else {
-                $(el).removeClass("is-invalid");
+                el.classList.remove("is-invalid");
             }
             return v;
         }
 
-        $("#history-from").val(histFrom);
-        $("#history-count").val(histCount);
-        $("#history-from, #history-count").on("input", (e) => {
-            validateAndClampInput(e.currentTarget);
-            ui.updateHistoryControlsState();
-        });
-        $("#history-from, #history-count").on("blur", (e) => {
-            const el = e.currentTarget;
-            const v = validateAndClampInput(el);
-            $(el).val(v).removeClass("is-invalid");
-            ui.updateHistoryControlsState();
-        });
-        $("#history-from,#history-count").on("change", () => {
-            histFrom = parseInt($("#history-from").val(), 10) || histFromDef;
-            histCount = parseInt($("#history-count").val(), 10) || historyCountDef;
+        document.getElementById("history-from").value = histFrom;
+        document.getElementById("history-count").value = histCount;
+        document.querySelectorAll("#history-from, #history-count").forEach((el) => {
+            el.addEventListener("input", () => {
+                validateAndClampInput(el);
+                ui.updateHistoryControlsState();
+            });
+            el.addEventListener("blur", () => {
+                const v = validateAndClampInput(el);
+                el.value = v;
+                el.classList.remove("is-invalid");
+                ui.updateHistoryControlsState();
+            });
+            el.addEventListener("change", () => {
+                histFrom = parseInt(document.getElementById("history-from").value, 10) || histFromDef;
+                histCount = parseInt(document.getElementById("history-count").value, 10) || historyCountDef;
+            });
         });
 
-        libft.set_page_size("history", $("#history_page_size").val());
+        libft.set_page_size("history", document.getElementById("history_page_size").value);
         libft.bindHistoryTableEventHandlers("history", 9);
 
-        $("#updateHistory").off("click");
-        $("#updateHistory").on("click", (e) => {
+        document.getElementById("updateHistory").addEventListener("click", (e) => {
             e.preventDefault();
             ui.getHistory();
         });
 
         // @reset history log
-        $("#resetHistory").off("click");
-        $("#resetHistory").on("click", (e) => {
+        document.getElementById("resetHistory").addEventListener("click", (e) => {
             e.preventDefault();
             if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert
                 return;
@@ -378,5 +380,10 @@ define(["jquery", "app/common", "app/libft", "app/tab-utils", "tabulator"],
             });
         });
 
+        document.getElementById("updateErrors").addEventListener("click", (e) => {
+            e.preventDefault();
+            ui.getErrors();
+        });
+
         return ui;
     });
index 637a595253416f00c77c24065011fbcf153bbf67..d16c75eee3d69b8006e2db7eb906ee90b756af9c 100644 (file)
@@ -1,18 +1,18 @@
-define(["jquery", "app/common"],
-    ($, common) => {
+define(["app/common"],
+    (common) => {
         "use strict";
         const ui = {};
         const fileSet = {files: null, index: null};
 
         function enable_disable_check_btn() {
-            $("#selectorsChkMsgBtn").prop("disabled", (
-                $.trim($("#selectorsMsgArea").val()).length === 0 ||
-                !$("#selectorsSelArea").hasClass("is-valid")
-            ));
+            const msgArea = document.getElementById("selectorsMsgArea");
+            const selArea = document.getElementById("selectorsSelArea");
+            document.getElementById("selectorsChkMsgBtn").disabled =
+                msgArea.value.trim().length === 0 || !selArea.classList.contains("is-valid");
         }
 
         function checkMsg(data) {
-            const selector = $("#selectorsSelArea").val();
+            const selector = document.getElementById("selectorsSelArea").value;
             common.query("plugins/selectors/check_message?selector=" + encodeURIComponent(selector), {
                 data: data,
                 method: "POST",
@@ -20,8 +20,8 @@ define(["jquery", "app/common"],
                     const json = neighbours_status[0].data;
                     if (json.success) {
                         common.alertMessage("alert-success", "Message successfully processed");
-                        $("#selectorsResArea")
-                            .val(Object.prototype.hasOwnProperty.call(json, "data") ? json.data.toString() : "");
+                        document.getElementById("selectorsResArea").value =
+                            Object.prototype.hasOwnProperty.call(json, "data") ? json.data.toString() : "";
                     } else {
                         common.alertMessage("alert-danger", "Unexpected error processing message");
                     }
@@ -31,11 +31,13 @@ define(["jquery", "app/common"],
         }
 
         function checkSelectors() {
+            const selArea = document.getElementById("selectorsSelArea");
             function toggle_form_group_class(remove, add) {
-                $("#selectorsSelArea").removeClass("is-" + remove).addClass("is-" + add);
+                selArea.classList.remove("is-" + remove);
+                selArea.classList.add("is-" + add);
                 enable_disable_check_btn();
             }
-            const selector = $("#selectorsSelArea").val();
+            const selector = selArea.value;
             if (selector.length && !common.read_only) {
                 common.query("plugins/selectors/check_selector?selector=" + encodeURIComponent(selector), {
                     method: "GET",
@@ -49,7 +51,7 @@ define(["jquery", "app/common"],
                     server: common.getServer()
                 });
             } else {
-                $("#selectorsSelArea").removeClass("is-valid is-invalid");
+                selArea.classList.remove("is-valid", "is-invalid");
                 enable_disable_check_btn();
             }
         }
@@ -57,11 +59,11 @@ define(["jquery", "app/common"],
         function buildLists() {
             function build_table_from_json(json, table_id) {
                 Object.keys(json).forEach((key) => {
-                    const td = $("<td/>");
-                    const tr = $("<tr/>")
-                        .append(td.clone().html("<code>" + key + "</code>"))
-                        .append(td.clone().html(json[key].description));
-                    $(table_id + " tbody").append(tr);
+                    const tr = common.el("tr", null,
+                        common.el("td", null, common.el("code", {text: key})),
+                        common.el("td", {text: json[key].description})
+                    );
+                    document.querySelector(table_id + " tbody").append(tr);
                 });
             }
 
@@ -82,53 +84,51 @@ define(["jquery", "app/common"],
 
         ui.displayUI = function () {
             if (!common.read_only &&
-                !$("#selectorsTable-extractors>tbody>tr").length &&
-                !$("#selectorsTable-transforms>tbody>tr").length) buildLists();
-            if (!$("#selectorsSelArea").is(".is-valid, .is-invalid")) checkSelectors();
+                !document.querySelector("#selectorsTable-extractors>tbody>tr") &&
+                !document.querySelector("#selectorsTable-transforms>tbody>tr")) buildLists();
+            if (!document.getElementById("selectorsSelArea").matches(".is-valid, .is-invalid")) checkSelectors();
         };
 
 
         function toggleSidebar(side) {
-            $("#sidebar-" + side).toggleClass("collapsed");
-            const openSidebarsCount = $("#sidebar-left").hasClass("collapsed") +
-                $("#sidebar-right").hasClass("collapsed");
+            document.getElementById("sidebar-" + side).classList.toggle("collapsed");
+            const openSidebarsCount = document.getElementById("sidebar-left").classList.contains("collapsed") +
+                document.getElementById("sidebar-right").classList.contains("collapsed");
             const layoutMap = {1: "col-lg-9", 2: "col-lg-12"};
             const contentClass = layoutMap[openSidebarsCount] || "col-lg-6";
-            $("#content").removeClass("col-lg-12 col-lg-9 col-lg-6")
-                .addClass(contentClass);
+            const content = document.getElementById("content");
+            content.classList.remove("col-lg-12", "col-lg-9", "col-lg-6");
+            content.classList.add(contentClass);
         }
-        $("#sidebar-tab-left>a").click(() => {
+        document.querySelector("#sidebar-tab-left > a").addEventListener("click", (e) => {
+            e.preventDefault();
             toggleSidebar("left");
-            return false;
         });
-        $("#sidebar-tab-right>a").click(() => {
+        document.querySelector("#sidebar-tab-right > a").addEventListener("click", (e) => {
+            e.preventDefault();
             toggleSidebar("right");
-            return false;
         });
 
-        $("#selectorsMsgClean").on("click", () => {
-            $("#selectorsChkMsgBtn").attr("disabled", true);
-            $("#selectorsMsgArea").val("");
-            return false;
+        document.getElementById("selectorsMsgClean").addEventListener("click", (e) => {
+            e.preventDefault();
+            document.getElementById("selectorsChkMsgBtn").disabled = true;
+            document.getElementById("selectorsMsgArea").value = "";
+            document.getElementById("selectorsFile").value = "";
         });
-        $("#selectorsClean").on("click", () => {
-            $("#selectorsSelArea").val("");
+        document.getElementById("selectorsClean").addEventListener("click", (e) => {
+            e.preventDefault();
+            document.getElementById("selectorsSelArea").value = "";
             checkSelectors();
-            return false;
         });
-        $("#selectorsChkMsgBtn").on("click", () => {
-            $("#selectorsResArea").val("");
-            checkMsg($("#selectorsMsgArea").val());
-            return false;
+        document.getElementById("selectorsChkMsgBtn").addEventListener("click", (e) => {
+            e.preventDefault();
+            document.getElementById("selectorsResArea").value = "";
+            checkMsg(document.getElementById("selectorsMsgArea").value);
         });
 
-        $("#selectorsSelArea").on("input", () => {
+        document.getElementById("selectorsSelArea").addEventListener("input", () => {
             checkSelectors();
         });
-        $("#selectorsMsgClean").on("click", () => {
-            $("#selectorsMsgArea").val("");
-            $("#selectorsFile").val("");
-        });
 
         common.fileUtils.setupFileHandling("#selectorsMsgArea", "#selectorsFile", fileSet, enable_disable_check_btn);
 
index c2d18c2ef2637df0ea5b5de692effee6588613f8..70508002382b55ef276a8f51bc69d9ef4dd384c9 100644 (file)
@@ -2,20 +2,31 @@
  * Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
  */
 
-define(["jquery", "app/common", "app/tab-utils", "tabulator"],
-    ($, common, tabUtils, Tabulator) => {
+define(["app/common", "app/tab-utils", "tabulator"],
+    (common, tabUtils, Tabulator) => {
         "use strict";
         const ui = {};
         let altered = {};
         let groupSelectEl = null;
 
+        function setSymbolsControlsDisabled(disabled) {
+            document.querySelectorAll("#refresh, #updateSymbols").forEach((el) => {
+                el.disabled = disabled;
+            });
+        }
+        function setSaveButtonsDisabled(disabled) {
+            document.querySelectorAll("#save-alert button").forEach((el) => {
+                el.disabled = disabled;
+            });
+        }
+
         function clear_altered() {
-            $("#save-alert").addClass("d-none");
+            document.getElementById("save-alert").classList.add("d-none");
             altered = {};
         }
 
         function saveSymbols(server) {
-            $("#save-alert button").attr("disabled", true);
+            setSaveButtonsDisabled(true);
 
             const values = [];
             Object.entries(altered).forEach(([key, value]) => values.push({name: key, value: value}));
@@ -25,7 +36,7 @@ define(["jquery", "app/common", "app/tab-utils", "tabulator"],
                     clear_altered();
                     common.alertMessage("alert-modal alert-success", "Symbols successfully saved");
                 },
-                complete: () => $("#save-alert button").removeAttr("disabled"),
+                complete: () => setSaveButtonsDisabled(false),
                 errorMessage: "Save symbols error",
                 method: "POST",
                 params: {
@@ -114,7 +125,7 @@ define(["jquery", "app/common", "app/tab-utils", "tabulator"],
 
         // @get symbols into modal form
         ui.getSymbols = function () {
-            $("#refresh, #updateSymbols").attr("disabled", true);
+            setSymbolsControlsDisabled(true);
             clear_altered();
             common.query("symbols", {
                 success: function (json) {
@@ -214,23 +225,25 @@ define(["jquery", "app/common", "app/tab-utils", "tabulator"],
 
                     common.tables.symbols.on("tableBuilt", () => {
                         if (common.read_only) {
-                            $(".mb-disabled").attr("disabled", true);
+                            document.querySelectorAll(".mb-disabled").forEach((el) => {
+                                el.disabled = true;
+                            });
                         }
-                        $("#refresh, #updateSymbols").removeAttr("disabled");
+                        setSymbolsControlsDisabled(false);
                     });
                     common.tables.symbols.on("renderComplete", () => {
-                        $("#refresh, #updateSymbols").removeAttr("disabled");
+                        setSymbolsControlsDisabled(false);
                     });
                 },
-                error: () => $("#refresh, #updateSymbols").removeAttr("disabled"),
+                error: () => setSymbolsControlsDisabled(false),
                 server: common.getServer()
             });
         };
 
 
-        $("#updateSymbols").on("click", (e) => {
+        document.getElementById("updateSymbols").addEventListener("click", (e) => {
             e.preventDefault();
-            $("#refresh, #updateSymbols").attr("disabled", true);
+            setSymbolsControlsDisabled(true);
             clear_altered();
             common.query("symbols", {
                 success: function (data) {
@@ -241,28 +254,27 @@ define(["jquery", "app/common", "app/tab-utils", "tabulator"],
                     common.tables.symbols.updateColumnDefinition("frequency", {formatterParams: freqParams});
                     common.tables.symbols.updateColumnDefinition("frequency_stddev", {formatterParams: freqParams});
                 },
-                error: () => $("#refresh, #updateSymbols").removeAttr("disabled"),
+                error: () => setSymbolsControlsDisabled(false),
                 server: common.getServer()
             });
         });
 
-        $("#symbolsTable")
-            .on("input", ".scorebar", ({target}) => {
-                const t = $(target);
-                t.removeClass("scorebar-ham scorebar-spam");
-                if (target.value < 0) {
-                    t.addClass("scorebar-ham");
-                } else if (target.value > 0) {
-                    t.addClass("scorebar-spam");
-                }
-            })
-            .on("change", ".scorebar", ({target}) => {
-                altered[$(target).attr("id").substring(5)] = parseFloat(target.value);
-                $("#save-alert").removeClass("d-none");
-            });
+        common.delegate("#symbolsTable", "input", ".scorebar", (event, target) => {
+            target.classList.remove("scorebar-ham", "scorebar-spam");
+            if (target.value < 0) {
+                target.classList.add("scorebar-ham");
+            } else if (target.value > 0) {
+                target.classList.add("scorebar-spam");
+            }
+        });
+        common.delegate("#symbolsTable", "change", ".scorebar", (event, target) => {
+            altered[target.id.substring(5)] = parseFloat(target.value);
+            document.getElementById("save-alert").classList.remove("d-none");
+        });
 
-        $("#save-alert button")
-            .on("click", ({target}) => saveSymbols($(target).data("save")));
+        document.querySelectorAll("#save-alert button").forEach((el) => {
+            el.addEventListener("click", () => saveSymbols(el.dataset.save));
+        });
 
         return ui;
     });