viz/specializers/rules/rule-visualizer.js

/**
 * Visualisiert einen DecisionTree als interaktive HTML-Liste.
 * Ermöglicht das An/Abschalten von Regeln per Checkbox.
 * @fileoverview
 */

class RuleVisualizer {
    /**
     * @param {string} containerId - ID des HTML Containers.
     * @param {DecisionTree} tree - Der Baum.
     * @param {function} toggleCallback - Wird gerufen, wenn Checkbox geklickt.
     */
    constructor(containerId, tree, toggleCallback) {
        this.container = document.getElementById(containerId);
        this.tree = tree;
        this.onToggle = toggleCallback; 
        this.prefix = containerId; 
    }

    render() {
        if (!this.container || !this.tree) return;
        this.container.innerHTML = `<div class="tree-root">${this._buildNodeHtml(this.tree.root)}</div>`;
        this.bindEvents();
    }

    _buildNodeHtml(node) {
        const id = `${this.prefix}-${node.name.replace(/\s+/g, '-')}`;
        const isChecked = node.active ? 'checked' : '';
        
        let html = `<div class="tree-node">`;
        html += `<div class="tree-content" id="${id}">`;
        
        // Icon Logik
        if (node.conditionFn) {
            // CONDITION
            html += `<span class="tree-condition-label">◇ ${node.name} ?</span>`;
        } else if (node.children && node.children.length > 0) {
            // GROUP
            html += `<span class="tree-group-label">📂 ${node.name}</span>`;
        } else {
            // RULE
            html += `<span class="tree-rule-label">📝 ${node.name}</span>`;
        }
        
        html += `<input type="checkbox" ${isChecked} data-name="${node.name}" class="rule-toggle">`;
        html += `</div>`; // Close Content

        // Kinder rendern
        if (node.children && node.children.length > 0) {
            for (const child of node.children) {
                if(child) html += this._buildNodeHtml(child);
            }
        }
        html += `</div>`; // Close Node
        return html;
    }

    bindEvents() {
        this.container.querySelectorAll('.rule-toggle').forEach(cb => {
            cb.addEventListener('change', (e) => {
                const name = e.target.getAttribute('data-name');
                if (this.onToggle) this.onToggle(name);
            });
        });
    }

    highlightCheck(name) { this._addClass(name, 'checking'); }
    highlightSuccess(name) { this._addClass(name, 'success'); }
    
    clearHighlights() {
        this.container.querySelectorAll('.checking, .success').forEach(el => {
            el.classList.remove('checking', 'success');
        });
    }
    
    _addClass(name, cls) {
        const id = `${this.prefix}-${name.replace(/\s+/g, '-')}`;
        const el = document.getElementById(id);
        if (el) {
            el.classList.remove('checking', 'success');
            el.classList.add(cls);
            if (cls === 'checking') el.scrollIntoView({ behavior: "smooth", block: "nearest" });
        }
    }
    
    _removeClass(name, cls) {
        const id = `${this.prefix}-${name.replace(/\s+/g, '-')}`;
        const el = document.getElementById(id);
        if (el) {
            el.classList.remove(cls);
        }
    }
}