ferro-json-ui 0.2.49

JSON-based server-driven UI schema types for Ferro
Documentation
pub(super) const SOURCE: &str = r#"
    // ── Dropdown menus (HTML popover API) ───────────────────────────────
    //
    // Markup:
    //   <button popovertarget="m1" ...>⋮</button>
    //   <div popover id="m1" data-popover-menu ...>…items…</div>
    //
    // The browser handles open/close, click-outside dismiss, Escape, and
    // promotes the panel to the top layer (escapes any overflow:hidden /
    // overflow:auto ancestor and any z-index stacking context for free).
    //
    // We supply only:
    //   - anchor positioning under the trigger on open
    //   - reposition on scroll/resize so the panel tracks its anchor
    //   - close when the trigger leaves the viewport
    //
    // Kanban cards override this in runtime/kanban.rs: there the whole
    // card is the trigger, not the kebab.

    function setupDropdowns() {
        var menus = document.querySelectorAll('[data-popover-menu]');
        for (var i = 0; i < menus.length; i++) {
            initPopoverMenu(menus[i]);
        }
    }

    function initPopoverMenu(panel) {
        var trigger = document.querySelector('[popovertarget="' + panel.id + '"]');
        if (!trigger) return;

        function track() {
            if (!panel.matches(':popover-open') || panel._kanbanFixed) return;
            var rect = trigger.getBoundingClientRect();
            // Close if the trigger has scrolled fully out of view.
            if (rect.bottom < 0 || rect.top > window.innerHeight) {
                panel.hidePopover();
                return;
            }
            positionUnderTrigger(panel, trigger);
        }

        panel.addEventListener('toggle', function(e) {
            if (e.newState === 'open' && !panel._kanbanFixed) {
                positionUnderTrigger(panel, trigger);
            }
        });
        // Capture=true so scrolls inside overflow:auto ancestors also fire.
        window.addEventListener('scroll', track, true);
        window.addEventListener('resize', track);
    }

    function positionUnderTrigger(panel, trigger) {
        // The popover opens with UA `inset: 0` which stretches the panel
        // to the viewport. The UA's `width: fit-content !important` is the
        // same value as what we want, but the layout doesn't re-evaluate
        // when we later clear `inset` to auto — so set it inline ourselves
        // to force a fresh sizing pass against the new (unconstrained) box.
        panel.style.setProperty('width', 'fit-content', 'important');
        panel.style.setProperty('min-width', '12rem', 'important');
        panel.style.inset = 'auto';
        panel.style.margin = '0';
        panel.style.position = 'fixed';
        void panel.offsetHeight; // flush layout so offsetWidth is accurate

        var rect = trigger.getBoundingClientRect();
        var pw = panel.offsetWidth || 192;
        var ph = panel.offsetHeight || 100;
        // Right-edge align with trigger, drop below by default.
        var left = rect.right - pw;
        var top = rect.bottom + 4;
        if (left < 8) left = 8;
        if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
        if (top + ph > window.innerHeight - 8) {
            var flipped = rect.top - ph - 4;
            if (flipped >= 8) top = flipped;
        }
        panel.style.left = left + 'px';
        panel.style.top = top + 'px';
    }
"#;