UNPKG

canoejs

Version:

A lightweight, widget-based UI framework

429 lines (363 loc) 13.1 kB
// Core import Widget from "./core/Widget"; import Render from "./core/Render"; import Router from "./core/Router"; import EventLinker from "./core/EventLinker"; // Utils import hashString from "./core/utils/hashStr"; import addHistoryEventsListener from "./core/utils/historyEvents"; import normalizeUrl from "./core/utils/normalizeUrl"; import randomId from "./core/utils/randomId"; import { memo, clearMemo, clearExpiredMemo } from "./core/utils/memo"; // Performance import { PerformanceManager } from "./core/config/Performance"; // Widgets import Alert from "./core/widgets/Alert"; import Badge from "./core/widgets/Badge"; import Button from "./core/widgets/Button"; import Card from "./core/widgets/Card"; import Col from "./core/widgets/Col"; import Container from "./core/widgets/Container"; import GroupedButtons from "./core/widgets/GroupedButtons"; import H from "./core/widgets/H"; import Input from "./core/widgets/Input"; import InputGroup from "./core/widgets/InputGroup"; import InputLabel from "./core/widgets/InputLabel"; import LazyWidget from "./core/widgets/LazyWidget"; import Link from "./core/widgets/Link"; import P from "./core/widgets/P"; import Progress from "./core/widgets/Progress"; import Row from "./core/widgets/Row"; import Spinner from "./core/widgets/Spinner"; import VirtualList from "./core/widgets/VirtualList"; import Modal from "./core/widgets/Modal"; import Tooltip from "./core/widgets/Tooltip"; import Toast, { ToastManager } from "./core/widgets/Toast"; // Enums import FlexAlignContent from "./core/enum/FlexAlignContent"; import FlexAlignItems from "./core/enum/FlexAlignItems"; import FlexJustify from "./core/enum/FlexJustify"; import FlexWrap from "./core/enum/Flexwrap"; import DefaultStyles from "./core/enum/defaultStyles"; // Theme import { ThemeProvider, defaultTheme, darkTheme } from "./core/theme/ThemeProvider"; // Animation import { AnimationManager } from "./core/animation/AnimationManager"; class Canoe { public static debug: boolean = false; private static rootId = ""; private static stateHash: string = ""; private static state: any = {}; private static renderer: Render; private static render: (state: any) => Widget; private static batchUpdates: boolean = false; private static pendingUpdates: any[] = []; private static renderScheduled: boolean = false; private static onLoadCallbacks: Function[] = []; private static postBuildCallbacks: Function[] = []; private static preBuildCallbacks: Function[] = []; // Sistema de suscripción al estado private static stateSubscribers: Map<string, Set<() => void>> = new Map(); // Sistema de gestión de widgets private static widgetInstances: Map<string, any> = new Map(); private static widgetUpdateQueue: Set<string> = new Set(); static setTitle(title: string): void { if (title) { document.title = title; } } static getState(): any { return this.state; } // Suscribirse a cambios de estado específicos static subscribeToState(stateKey: string, callback: () => void): () => void { if (!this.stateSubscribers.has(stateKey)) { this.stateSubscribers.set(stateKey, new Set()); } this.stateSubscribers.get(stateKey)!.add(callback); // Retornar función para desuscribirse return () => { this.stateSubscribers.get(stateKey)?.delete(callback); }; } // Registrar una instancia de widget static registerWidget(widgetId: string, widget: any): void { this.widgetInstances.set(widgetId, widget); } // Desregistrar una instancia de widget static unregisterWidget(widgetId: string): void { this.widgetInstances.delete(widgetId); } // Marcar un widget para actualización static markWidgetForUpdate(widgetId: string): void { this.widgetUpdateQueue.add(widgetId); } // Actualizar todos los widgets marcados private static updateMarkedWidgets(): void { this.widgetUpdateQueue.forEach(widgetId => { const widget = this.widgetInstances.get(widgetId); if (widget && typeof widget.forceUpdate === 'function') { try { widget.forceUpdate(); } catch (error) { console.error(`Error updating widget ${widgetId}:`, error); } } }); this.widgetUpdateQueue.clear(); } // Notificar a los suscriptores de cambios específicos private static notifySubscribers(changedKeys: string[]): void { changedKeys.forEach(key => { const subscribers = this.stateSubscribers.get(key); if (subscribers) { subscribers.forEach(callback => { try { callback(); } catch (error) { console.error('Error in state subscriber:', error); } }); } }); } static onLoad(callback: Function): void { this.onLoadCallbacks.push(callback); } static postBuild(callback: Function): void { this.postBuildCallbacks.push(callback); } static preBuild(callback: Function): void { this.preBuildCallbacks.push(callback); } static buildApp(rootId: string, initialState: any, renderFn: (state: any) => Widget): Render { this.onLoadCallbacks.forEach((callback) => { callback(); }); // Bind history events listeners addHistoryEventsListener(); this.rootId = rootId; this.render = renderFn; this._setState(initialState, false); this.renderer = new Render(rootId, renderFn(initialState)); return this.renderer; } public static navigate(url: string): void { // if url is another site or has http, open in new tab if (url.includes("http") || url.includes("www")) { window.open(url, "_blank"); return; } else { window.history.pushState({}, "", url); } } public static setState(newState: any): void { if (this.batchUpdates) { this.pendingUpdates.push(newState); this.scheduleRender(); return; } this._setState(newState, true); } // Forzar re-renderizado incluso si el hash no cambió public static forceRender(): void { if (this.renderer) { this.renderer.rootWidget = this.render(this.state); this.renderer.render(); } } // Comparar valores de estado de manera más robusta private static hasStateChanged(oldValue: any, newValue: any): boolean { // Si son el mismo objeto, no han cambiado if (oldValue === newValue) return false; // Si uno es null/undefined y el otro no, han cambiado if ((oldValue == null) !== (newValue == null)) return true; // Si ambos son null/undefined, no han cambiado if (oldValue == null && newValue == null) return false; // Si son de tipos diferentes, han cambiado if (typeof oldValue !== typeof newValue) return true; // Para objetos y arrays, usar JSON.stringify para comparación profunda if (typeof oldValue === 'object') { try { return JSON.stringify(oldValue) !== JSON.stringify(newValue); } catch (error) { // Si hay error en la serialización, asumir que cambiaron return true; } } // Para valores primitivos, comparación directa return oldValue !== newValue; } public static batchUpdate(updates: (() => void)[]): void { this.batchUpdates = true; updates.forEach(update => update()); this.batchUpdates = false; if (this.pendingUpdates.length > 0) { const mergedState = this.pendingUpdates.reduce((acc, update) => ({ ...acc, ...update }), {}); this.pendingUpdates = []; this._setState(mergedState, true); } } private static scheduleRender(): void { if (!this.renderScheduled) { this.renderScheduled = true; requestAnimationFrame(() => { this.renderScheduled = false; if (this.pendingUpdates.length > 0) { const mergedState = this.pendingUpdates.reduce((acc, update) => ({ ...acc, ...update }), {}); this.pendingUpdates = []; this._setState(mergedState, true); } }); } } static _setState(newState: any, rebuild = true): Promise<void> { this.preBuildCallbacks.forEach((callback) => { callback(); }); // Detectar qué claves cambiaron const changedKeys: string[] = []; Object.keys(newState).forEach(key => { const oldValue = this.state[key]; const newValue = newState[key]; // Comparación más robusta que maneja objetos anidados const hasChanged = this.hasStateChanged(oldValue, newValue); if (hasChanged) { changedKeys.push(key); } }); // Update the state Object.assign(this.state, newState); // Notificar a los suscriptores de las claves que cambiaron if (changedKeys.length > 0) { this.notifySubscribers(changedKeys); } // Actualizar widgets marcados this.updateMarkedWidgets(); // Generate hash of state (optimized) - pero solo si hay cambios reales let newHash = ""; if (changedKeys.length > 0) { newHash = hashString(JSON.stringify(this.state, (_, value) => typeof value === "function" ? value.toString() : value )); } else { newHash = this.stateHash; // Mantener el hash anterior si no hay cambios } // Si no hay cambios reales, no hacer nada if (newHash === this.stateHash && changedKeys.length === 0) { return Promise.resolve(); } // Update the hash this.stateHash = newHash; // Re-render the app using existing renderer if (rebuild && this.renderer) { // Update the root widget with new state this.renderer.rootWidget = this.render(this.state); this.renderer.render(); } this.postBuildCallbacks.forEach((callback) => { callback(); }); return Promise.resolve(); } static clearCache(): void { Render.clearCache(); Router.clearCache(); } static help() { console.log("CanoeJS API disponible:"); console.log(Object.getOwnPropertyNames(Canoe).filter(k => typeof (Canoe as any)[k] === 'function')); console.log("Ejemplo: Canoe.getState(), Canoe.setState({...}), Canoe.navigate('/docs'), etc."); } // Método de debug para diagnosticar problemas de re-renderizado static debugRender(newState: any): void { console.log("🔍 Debug Render:"); console.log("Estado actual:", this.state); console.log("Nuevo estado:", newState); const changedKeys: string[] = []; Object.keys(newState).forEach(key => { const oldValue = this.state[key]; const newValue = newState[key]; const hasChanged = this.hasStateChanged(oldValue, newValue); if (hasChanged) { changedKeys.push(key); console.log(` ✅ Cambio detectado en '${key}':`, { old: oldValue, new: newValue }); } else { console.log(` ❌ Sin cambios en '${key}':`, { old: oldValue, new: newValue }); } }); console.log("Claves que cambiaron:", changedKeys); console.log("Hash actual:", this.stateHash); } // Método de debug para verificar el estado de los eventos static debugEvents(): void { console.log("🎯 Debug Eventos:"); const stats = EventLinker.getEventStats(); console.log("Estadísticas de eventos:", stats); const duplicateCheck = EventLinker.checkForDuplicates(); if (duplicateCheck.hasDuplicates) { console.warn("⚠️ Eventos duplicados detectados:", duplicateCheck.duplicates); } else { console.log("✅ No se detectaron eventos duplicados"); } // Verificar si hay elementos con múltiples event listeners const elementsWithEvents = new Map<string, number>(); EventLinker.events.forEach(event => { const count = elementsWithEvents.get(event.elementAtId) || 0; elementsWithEvents.set(event.elementAtId, count + 1); }); const elementsWithMultipleEvents = Array.from(elementsWithEvents.entries()) .filter(([_, count]) => count > 1); if (elementsWithMultipleEvents.length > 0) { console.log("📊 Elementos con múltiples eventos:", elementsWithMultipleEvents); } } } export { Canoe, EventLinker, Render, Router, Widget, Alert, Badge, Button, Card, Col, Container, GroupedButtons, H, Input, InputGroup, InputLabel, LazyWidget, Link, P, Progress, Row, Spinner, VirtualList, Modal, Tooltip, Toast, ToastManager, FlexAlignContent, FlexAlignItems, FlexJustify, FlexWrap, DefaultStyles, hashString, addHistoryEventsListener, normalizeUrl, randomId, memo, clearMemo, clearExpiredMemo, PerformanceManager, // Theme ThemeProvider, defaultTheme, darkTheme, // Animation AnimationManager };