HEX
Server: Apache/2
System: Linux nexus-01 4.18.0-553.146.1.el8_10.x86_64 #1 SMP Tue Jul 21 03:06:01 EDT 2026 x86_64
User: aglcoke (1118)
PHP: 8.2.32
Disabled: mail,exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: //proc/1/cwd/usr/share/rspamd/www/js/app/tab-utils.js
/*
 * Tabulator UI helpers shared across tables.
 * Extracted from history.js Phase 0. Depends only on common (for
 * common.tables); no jQuery.
 */

define(["app/common"],
    (common) => {
        "use strict";
        const tabUtils = {};

        let scrollIntoViewPatched = false;
        // Tabulator's destroy() clears a table's children but leaves the mount
        // element (and listeners attached to it) in place, so a destroy+re-init
        // would stack handlers. Keyed by element to keep one handler per table.
        const rowToggleHandlers = new WeakMap();
        // Same leak mode as rowToggleHandlers, but installScrollPreservation also
        // binds a window "scroll" listener and (optionally) arm-trigger listeners;
        // all stored here so a rebuild can remove the previous set first.
        const scrollPreservationHandlers = new WeakMap();

        /**
         * Patch Element.prototype.scrollIntoView once so that calls for elements
         * inside ANY Tabulator table (.tabulator container) are no-ops. Guarded
         * by a module-level flag so the prototype is patched only once regardless
         * of how many tables call this.
         */
        tabUtils.patchScrollIntoViewOnce = function () {
            if (scrollIntoViewPatched) return;
            const native = Element.prototype.scrollIntoView;
            Element.prototype.scrollIntoView = function (...args) {
                if (!this.closest(".tabulator")) {
                    native.apply(this, args);
                }
            };
            scrollIntoViewPatched = true;
        };

        /**
         * Remove tabindex from the tableholder and keep it removed (Tabulator
         * re-adds it on render). Prevents focus-scroll when clicking body cells.
         */
        tabUtils.stripTableholderTabindex = function (table) {
            const holder = common.tables[table].element.querySelector(".tabulator-tableholder");
            if (!holder) return;
            holder.removeAttribute("tabindex");
            new MutationObserver(() => holder.removeAttribute("tabindex"))
                .observe(holder, {attributes: true, attributeFilter: ["tabindex"]});
        };

        /**
         * Hide the pagination footer when the table has only one page. Registers
         * a renderComplete handler.
         */
        tabUtils.hideFooterOnSinglePage = function (table) {
            common.tables[table].on("renderComplete", () => {
                const t = common.tables[table];
                const footer = t.element.querySelector(".tabulator-footer");
                if (footer) footer.style.display = t.getPageMax() > 1 ? "" : "none";
            });
        };

        /**
         * Allow clicking anywhere on a row to toggle responsive-collapse
         * (instead of just the tiny toggle icon). Skips when selecting text,
         * clicking form elements (inputs, selects — e.g. the Score column),
         * and when collapse is not active (toggle hidden).
         *
         * Idempotent across destroy+re-init: destroy() leaves the mount element
         * and its listeners in place, so without removing the previous handler a
         * rebuild would bind a second one and row clicks would no-op (the two
         * handlers toggle the row open then shut).
         */
        tabUtils.bindRowClickToggle = function (table) {
            const el = common.tables[table].element;
            const prev = rowToggleHandlers.get(el);
            if (prev) el.removeEventListener("click", prev);
            function handler(e) {
                const row = e.target.closest(".tabulator-row");
                if (!row) return;
                if (e.target.closest("input, select, textarea, button, .sym-order-toggle")) return;
                const sel = window.getSelection && window.getSelection();
                if (sel && sel.toString()) return;
                const toggle = row.querySelector(".tabulator-responsive-collapse-toggle");
                if (toggle && toggle.offsetParent) toggle.click();
            }
            el.addEventListener("click", handler);
            rowToggleHandlers.set(el, handler);
        };

        /**
         * Install scroll-position preservation for a table. Tabulator scrolls
         * the page on interactions; this restores the position synchronously
         * (before paint) so the jump is never visible.
         *
         * @param {string} table - Key in common.tables
         * @param {Object} options - { armTriggers: HTMLElement[], clickMs?: 250, renderMs?: 400 }
         */
        tabUtils.installScrollPreservation = function (table, options) {
            const t = common.tables[table];
            const el = t.element;
            const {clickMs = 250, renderMs = 400, armTriggers = []} = options || {};

            // destroy()+re-init leaves the mount element (and window) in place, so
            // each rebuild would stack a fresh listener set that is never released
            // — a memory/listener leak. Tear down the previous set first. Mirrors
            // bindRowClickToggle's WeakMap guard; keyed by the mount element.
            const prev = scrollPreservationHandlers.get(el);
            if (prev) {
                ["click", "change", "input"].forEach((evt) => el.removeEventListener(evt, prev.arm, true));
                prev.armTriggerEls.forEach((trigger) => trigger.removeEventListener("click", prev.arm, true));
                window.removeEventListener("scroll", prev.onScroll, true);
            }

            let preserveY = 0;
            let preserveUntil = 0;
            let clickArmed = false;

            function arm() {
                const y = window.scrollY;
                preserveY = y;
                preserveUntil = performance.now() + clickMs;
                clickArmed = true;
                Promise.resolve().then(() => {
                    // Microtask (after sync handlers, before paint): catch sync scrolls.
                    if (window.scrollY !== y) window.scrollTo(0, y);
                    // rAF queued from the microtask lands AFTER Tabulator's render rAF
                    // (queued during the click), in the same frame after the render-
                    // scroll but before paint.
                    requestAnimationFrame(() => {
                        if (window.scrollY !== y) window.scrollTo(0, y);
                    });
                });
            }

            function onScroll() {
                if (performance.now() >= preserveUntil) return;
                if (window.scrollY !== preserveY) window.scrollTo(0, preserveY);
            }

            // Arm on click plus change/input. A header-filter <select> changes
            // (and triggers a filter re-render) long after its opening click, by
            // which time an intervening renderComplete may have disarmed us;
            // re-arming on change/input keeps the preservation live for that render.
            ["click", "change", "input"].forEach((evt) => el.addEventListener(evt, arm, true));
            armTriggers.forEach((trigger) => trigger.addEventListener("click", arm, true));

            t.on("renderStarted", () => {
                if (clickArmed) preserveUntil = performance.now() + renderMs;
            });
            t.on("renderComplete", () => {
                // The render path scrolls the window asynchronously — the scroll
                // event (and the listener below) lands a frame late, so the jump
                // flashes for one frame. Restore synchronously here, before paint.
                if (clickArmed && window.scrollY !== preserveY) {
                    window.scrollTo(0, preserveY);
                }
                clickArmed = false;
            });

            window.addEventListener("scroll", onScroll, true);

            scrollPreservationHandlers.set(el, {arm, onScroll, armTriggerEls: armTriggers});
        };

        return tabUtils;
    });