ferro-json-ui 0.3.11

JSON-based server-driven UI schema types for Ferro
Documentation
pub(super) const SOURCE: &str = r#"
    // ── Bulk action bar ────────────────────────────────────────────────────
    //
    // Floating action bar at viewport bottom, visible when ≥1 DataTable rows
    // are checked via bulk-select checkboxes with data-row-key. Tracks
    // selection state; destructive actions require native <dialog> confirm.
    // Idempotent via data-bulk-bar-init guard. Degrades gracefully when
    // #fjui-bulk-bar or #fjui-bulk-confirm are absent from the layout.

    function updateBulkBar(selected, bar, countEl) {
        try {
            var count = Object.keys(selected).length;
            if (!bar) return;
            bar.style.display = count > 0 ? 'flex' : 'none';
            bar.setAttribute('aria-label', 'Azioni su ' + count + ' elementi selezionati');
            if (countEl) countEl.textContent = count + ' selezionati';
        } catch (_) {}
    }

    function executeBulkAction(keys, action, endpoint, selected, bar, countEl) {
        try {
            var body = 'action=' + encodeURIComponent(action);
            for (var i = 0; i < keys.length; i++) {
                body += '&keys[]=' + encodeURIComponent(keys[i]);
            }
            fetch(endpoint, {
                method: 'POST',
                credentials: 'same-origin',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body
            }).then(function(r) {
                if (r.ok) {
                    // Clear selection and hide bar on success.
                    var keysArr = Object.keys(selected);
                    for (var k = 0; k < keysArr.length; k++) {
                        delete selected[keysArr[k]];
                    }
                    // Uncheck all checkboxes in bulk-select tables.
                    var cbs = document.querySelectorAll('[data-bulk-select-table] input[type="checkbox"]');
                    for (var j = 0; j < cbs.length; j++) {
                        cbs[j].checked = false;
                    }
                    updateBulkBar(selected, bar, countEl);
                }
            }).catch(function() {});
        } catch (_) {}
    }

    function confirmBulkAction(keys, action, endpoint, selected, bar, countEl) {
        var dlg = document.getElementById('fjui-bulk-confirm');
        if (!dlg || typeof dlg.showModal !== 'function') {
            // Degrade: dialog absent or not supported — fall back to native confirm
            // so the user still sees a confirmation prompt before a destructive action
            // executes (IN-02). A missing dialog may indicate a render bug; the native
            // confirm prevents silent data loss in that case.
            var msg = 'Confermi l\'operazione su ' + keys.length + ' elementi?';
            if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
                if (!window.confirm(msg)) { return; }
            }
            executeBulkAction(keys, action, endpoint, selected, bar, countEl);
            return;
        }
        dlg.addEventListener('close', function handler() {
            dlg.removeEventListener('close', handler);
            if (dlg.returnValue === 'confirm') {
                executeBulkAction(keys, action, endpoint, selected, bar, countEl);
            }
        });
        try { dlg.showModal(); } catch (_) {
            executeBulkAction(keys, action, endpoint, selected, bar, countEl);
        }
    }

    function attachBulkBarBehavior(table) {
        var bar = document.getElementById('fjui-bulk-bar');
        var countEl = bar ? bar.querySelector('[data-bulk-count]') : null;
        var selected = {};

        // Delegated change listener on the table container covers checkboxes
        // injected after a nav swap.
        table.addEventListener('change', function(e) {
            try {
                var cb = e.target;
                if (!cb || cb.type !== 'checkbox') return;
                var key = cb.getAttribute('data-row-key');
                if (!key) return;
                if (cb.checked) {
                    selected[key] = true;
                } else {
                    delete selected[key];
                }
                updateBulkBar(selected, bar, countEl);
            } catch (_) {}
        });

        // Select-all header checkbox.
        table.addEventListener('change', function(e) {
            try {
                var cb = e.target;
                if (!cb || cb.type !== 'checkbox') return;
                var isHeaderCb = cb.closest('thead') !== null;
                if (!isHeaderCb) return;
                var cbs = table.querySelectorAll('tbody input[type="checkbox"]');
                for (var i = 0; i < cbs.length; i++) {
                    cbs[i].checked = cb.checked;
                    var key = cbs[i].getAttribute('data-row-key');
                    if (!key) continue;
                    if (cb.checked) {
                        selected[key] = true;
                    } else {
                        delete selected[key];
                    }
                }
                updateBulkBar(selected, bar, countEl);
            } catch (_) {}
        });

        // Action buttons in the bar.
        if (bar) {
            bar.addEventListener('click', function(e) {
                try {
                    var btn = e.target.closest('[data-bulk-action]');
                    if (!btn) return;
                    var action = btn.getAttribute('data-bulk-action');
                    var endpoint = btn.getAttribute('data-bulk-endpoint') || '';
                    var isDestructive = btn.hasAttribute('data-bulk-destructive');
                    var keys = Object.keys(selected);
                    if (keys.length === 0) return;
                    if (isDestructive) {
                        confirmBulkAction(keys, action, endpoint, selected, bar, countEl);
                    } else {
                        executeBulkAction(keys, action, endpoint, selected, bar, countEl);
                    }
                } catch (_) {}
            });

            // Dismiss button clears selection.
            var dismissBtn = bar.querySelector('[data-bulk-dismiss]');
            if (dismissBtn) {
                dismissBtn.addEventListener('click', function() {
                    try {
                        var keysArr = Object.keys(selected);
                        for (var k = 0; k < keysArr.length; k++) {
                            delete selected[keysArr[k]];
                        }
                        var cbs = document.querySelectorAll('[data-bulk-select-table] input[type="checkbox"]');
                        for (var j = 0; j < cbs.length; j++) {
                            cbs[j].checked = false;
                        }
                        updateBulkBar(selected, bar, countEl);
                    } catch (_) {}
                });
            }
        }
    }

    function setupBulkBar() {
        try {
            var tables = document.querySelectorAll('[data-bulk-select-table]');
            for (var i = 0; i < tables.length; i++) {
                if (tables[i].dataset.bulkBarInit) continue;
                tables[i].dataset.bulkBarInit = '1';
                attachBulkBarBehavior(tables[i]);
            }
        } catch (_) {}
        document.addEventListener('fjui:navigated', function() {
            try {
                var tables = document.querySelectorAll('[data-bulk-select-table]');
                for (var i = 0; i < tables.length; i++) {
                    if (tables[i].dataset.bulkBarInit) continue;
                    tables[i].dataset.bulkBarInit = '1';
                    attachBulkBarBehavior(tables[i]);
                }
            } catch (_) {}
        });
    }
"#;