web_ui 0.1.1

A simple Rust library for creating local web interfaces with real-time communication
Documentation
/**
 * WebUI JavaScript Client Library
 * Provides easy binding of HTML elements to Rust backend functions
 */

class WebUIClient {
    constructor() {
        this.useWebSocket = true;
        this.websocket = null;
        this.eventQueue = [];
        this.pendingRequests = new Map();
        this.requestId = 0;
        this.reconnectDelay = 1000;
        this.maxReconnectDelay = 30000;
        this.currentReconnectDelay = this.reconnectDelay;
        
        this.init();
    }

    init() {
        this.setupWebSocket();
        
        // Fallback to HTTP if WebSocket fails
        setTimeout(() => {
            if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
                console.log('WebSocket connection failed, falling back to HTTP');
                this.useWebSocket = false;
            }
        }, 2000);
    }

    setupWebSocket() {
        try {
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsUrl = `${protocol}//${window.location.host}/ws`;
            
            this.websocket = new WebSocket(wsUrl);
            
            this.websocket.onopen = () => {
                console.log('WebSocket connected');
                this.useWebSocket = true;
                this.currentReconnectDelay = this.reconnectDelay;
                this.processEventQueue();
            };
            
            this.websocket.onmessage = (event) => {
                try {
                    const response = JSON.parse(event.data);
                    this.handleResponse(response);
                } catch (error) {
                    console.error('Error parsing WebSocket message:', error);
                }
            };
            
            this.websocket.onclose = () => {
                console.log('WebSocket disconnected');
                this.useWebSocket = false;
                this.scheduleReconnect();
            };
            
            this.websocket.onerror = (error) => {
                console.error('WebSocket error:', error);
                this.useWebSocket = false;
            };
        } catch (error) {
            console.error('Failed to create WebSocket:', error);
            this.useWebSocket = false;
        }
    }

    scheduleReconnect() {
        setTimeout(() => {
            console.log('Attempting to reconnect WebSocket...');
            this.setupWebSocket();
            this.currentReconnectDelay = Math.min(this.currentReconnectDelay * 1.5, this.maxReconnectDelay);
        }, this.currentReconnectDelay);
    }

    async sendEvent(elementId, eventType, data = {}) {
        const event = {
            element_id: elementId,
            event_type: eventType,
            data: data
        };

        if (this.useWebSocket && this.websocket && this.websocket.readyState === WebSocket.OPEN) {
            return this.sendEventViaWebSocket(event);
        } else {
            return this.sendEventViaHTTP(event);
        }
    }

    sendEventViaWebSocket(event) {
        return new Promise((resolve, reject) => {
            try {
                const requestId = ++this.requestId;
                event.request_id = requestId;
                
                this.pendingRequests.set(requestId, { resolve, reject });
                this.websocket.send(JSON.stringify(event));
                
                // Timeout after 10 seconds
                setTimeout(() => {
                    if (this.pendingRequests.has(requestId)) {
                        this.pendingRequests.delete(requestId);
                        reject(new Error('Request timeout'));
                    }
                }, 10000);
            } catch (error) {
                reject(error);
            }
        });
    }

    async sendEventViaHTTP(event) {
        try {
            const response = await fetch('/api/event', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(event)
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error('HTTP request failed:', error);
            throw error;
        }
    }

    handleResponse(response) {
        if (response.request_id && this.pendingRequests.has(response.request_id)) {
            const { resolve } = this.pendingRequests.get(response.request_id);
            this.pendingRequests.delete(response.request_id);
            resolve(response);
        }
    }

    processEventQueue() {
        while (this.eventQueue.length > 0) {
            const event = this.eventQueue.shift();
            this.sendEventViaWebSocket(event.event)
                .then(event.resolve)
                .catch(event.reject);
        }
    }

    // Convenience methods for common events
    bindClick(elementId, callback) {
        return this.bindEvent(elementId, 'click', callback);
    }

    bindChange(elementId, callback) {
        return this.bindEvent(elementId, 'change', callback);
    }

    bindSubmit(elementId, callback) {
        return this.bindEvent(elementId, 'submit', callback);
    }

    bindEvent(elementId, eventType, callback) {
        const element = document.getElementById(elementId);
        if (!element) {
            console.error(`Element with ID '${elementId}' not found`);
            return;
        }

        element.addEventListener(eventType, async (domEvent) => {
            try {
                // Prevent default for forms
                if (eventType === 'submit') {
                    domEvent.preventDefault();
                }

                // Collect relevant data from the DOM event
                const eventData = this.extractEventData(domEvent, element);
                
                // Send event to backend
                const response = await this.sendEvent(elementId, eventType, eventData);
                
                // Call the callback with the response
                if (callback) {
                    callback(response, domEvent);
                }
                
                if (!response.success && response.message) {
                    console.error('Backend error:', response.message);
                }
            } catch (error) {
                console.error('Error handling event:', error);
                if (callback) {
                    callback({ success: false, message: error.message }, domEvent);
                }
            }
        });
    }

    extractEventData(domEvent, element) {
        const data = {};
        
        // Add common properties
        if (element.value !== undefined) {
            data.value = element.value;
        }
        
        if (element.checked !== undefined) {
            data.checked = element.checked;
        }
        
        // For forms, collect all form data
        if (element.tagName === 'FORM') {
            const formData = new FormData(element);
            data.formData = {};
            for (let [key, value] of formData.entries()) {
                data.formData[key] = value;
            }
        }
        
        // For buttons, collect related input values (if button has data-collect attribute or is a greet button)
        if (element.tagName === 'BUTTON' && (element.hasAttribute('data-collect') || element.id === 'greet-btn')) {
            // Collect values from specific inputs or all inputs in the same container
            const container = element.closest('div') || document;
            const inputs = container.querySelectorAll('input, select, textarea');
            inputs.forEach(input => {
                if (input.id) {
                    data[input.id] = input.value;
                }
            });
        }
        
        // Add mouse/keyboard event data if available
        if (domEvent.clientX !== undefined) {
            data.mouse = {
                x: domEvent.clientX,
                y: domEvent.clientY
            };
        }
        
        if (domEvent.key !== undefined) {
            data.key = domEvent.key;
        }
        
        return data;
    }

    // Utility method to bind multiple elements at once
    bindElements(bindings) {
        for (const binding of bindings) {
            this.bindEvent(binding.elementId, binding.eventType, binding.callback);
        }
    }
}

// Create global instance
window.webui = new WebUIClient();

// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
    module.exports = WebUIClient;
}