ferro-json-ui 0.2.36

JSON-based server-driven UI schema types for Ferro
Documentation
pub(super) const SOURCE: &str = r#"
    // ── Scroll preservation across form actions ───────────────────────────
    //
    // Many dashboard interactions (kanban "Avanza", "Pagato in contanti",
    // delete, edit) POST and redirect back to the same page. Without help
    // the browser drops the user back at the top and any inner-scrolled
    // panel (kanban column, <main> in the DashboardLayout) is reset.
    //
    // Three scroll surfaces are captured on submit, keyed by current
    // pathname, and restored on the next DOMContentLoaded if the
    // destination pathname matches:
    //
    //   1. `window` (window.scrollY) — pages where the document scrolls.
    //   2. `main` (<main>.scrollTop) — DashboardLayout pins html/body to
    //      100vh and lets only <main> overflow; window.scrollY is always 0
    //      on those pages. Captured by tag name so the same key restores
    //      across the DOM regeneration that a 302 redirect triggers.
    //   3. `.ferro-kanban-scroll` columns — matched by index since the
    //      DOM is regenerated on the destination page.
    //
    // Stale entries are read-once: removed from sessionStorage as soon as
    // they're applied, so they don't leak into unrelated future visits.

    function setupScrollPreserve() {
        var KEY = 'ferro:scroll:' + location.pathname;

        // Restore first — synchronously before any other setup mutates layout.
        try {
            var raw = sessionStorage.getItem(KEY);
            if (raw) {
                sessionStorage.removeItem(KEY);
                var state = JSON.parse(raw);
                // Defer to next frame so the browser has finished layout —
                // scrollTop on an `overflow:auto` element only sticks once the
                // content height is known.
                requestAnimationFrame(function() {
                    if (typeof state.window === 'number') {
                        window.scrollTo(0, state.window);
                    }
                    if (typeof state.main === 'number') {
                        var main = document.querySelector('main');
                        if (main) main.scrollTop = state.main;
                    }
                    var scrollers = document.querySelectorAll('.ferro-kanban-scroll');
                    if (state.scrollers && state.scrollers.length === scrollers.length) {
                        for (var i = 0; i < scrollers.length; i++) {
                            scrollers[i].scrollTop = state.scrollers[i] || 0;
                        }
                    }
                });
            }
        } catch (e) { /* sessionStorage may be unavailable */ }

        // Capture on submit. Use capture phase so we run before any
        // preventDefault handlers might cancel the navigation.
        document.addEventListener('submit', function() {
            try {
                var state = { window: window.scrollY, main: 0, scrollers: [] };
                var main = document.querySelector('main');
                if (main) state.main = main.scrollTop;
                var scrollers = document.querySelectorAll('.ferro-kanban-scroll');
                for (var i = 0; i < scrollers.length; i++) {
                    state.scrollers.push(scrollers[i].scrollTop);
                }
                sessionStorage.setItem(KEY, JSON.stringify(state));
            } catch (e) { /* ignore quota / privacy errors */ }
        }, true);
    }
"#;