BOM (Browser Object Model)


¿Qué es el BOM?

El BOM (Browser Object Model) es el conjunto de objetos JavaScript que proporcionan acceso a las funcionalidades del navegador más allá del documento HTML. Mientras el DOM se enfoca en manipular el contenido de la página, el BOM te permite controlar el navegador mismo: ventanas, historial, ubicación, almacenamiento y más.

¿Para qué se utiliza el BOM?

El BOM es utilizado por desarrolladores para:

  • Controlar ventanas y pestañas del navegador (abrir, cerrar, redimensionar).
  • Navegar por el historial del navegador (back, forward, go).
  • Manipular la URL actual sin recargar la página.
  • Detectar información del navegador y sistema operativo.
  • Gestionar cookies y almacenamiento local.
  • Mostrar alertas, confirmaciones y prompts al usuario.
  • Medir el rendimiento y tiempos de carga de páginas.

¿Cómo funciona?

El BOM funciona como el panel de control del navegador - te da acceso a todas las palancas, botones y configuraciones que controlan cómo funciona la ventana del navegador. Es como tener los controles de la cabina de un avión, pero para tu aplicación web.

El objeto Window: la raíz del BOM

window es el objeto global que representa la ventana del navegador:

// window es el objeto global
console.log(window); // Objeto Window completo

// En el navegador, todas las variables globales son propiedades de window
var globalVar = 'Soy global';
console.log(window.globalVar); // 'Soy global'

// Funciones globales también son métodos de window
function globalFunction() {
  console.log('Función global');
}
window.globalFunction(); // 'Función global'

// window también contiene el objeto document (DOM)
console.log(window.document === document); // true

// Propiedades básicas de window
console.log('Ancho interno:', window.innerWidth);
console.log('Alto interno:', window.innerHeight);
console.log('Ancho externo:', window.outerWidth);
console.log('Alto externo:', window.outerHeight);
console.log('Posición X:', window.screenX);
console.log('Posición Y:', window.screenY);

Diálogos del navegador

// Alert - Mostrar mensaje simple
window.alert('¡Hola! Este es un alert');

// Confirm - Obtener confirmación del usuario
const userConfirmed = window.confirm('¿Estás seguro?');
if (userConfirmed) {
  console.log('Usuario confirmó');
} else {
  console.log('Usuario canceló');
}

// Prompt - Solicitar entrada de texto
const userName = window.prompt('¿Cuál es tu nombre?', 'Visitante');
if (userName !== null) {
  console.log('Hola,', userName);
} else {
  console.log('Usuario canceló el prompt');
}

// Clase helper para diálogos personalizados
class CustomDialog {
  static alert(message, title = 'Alerta') {
    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.className = 'custom-dialog';
      dialog.innerHTML = `
                <div class="dialog-overlay"></div>
                <div class="dialog-content">
                    <h3>${title}</h3>
                    <p>${message}</p>
                    <button class="btn-ok">Aceptar</button>
                </div>
            `;

      document.body.appendChild(dialog);

      dialog.querySelector('.btn-ok').addEventListener('click', () => {
        document.body.removeChild(dialog);
        resolve();
      });
    });
  }

  static confirm(message, title = 'Confirmar') {
    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.className = 'custom-dialog';
      dialog.innerHTML = `
                <div class="dialog-overlay"></div>
                <div class="dialog-content">
                    <h3>${title}</h3>
                    <p>${message}</p>
                    <button class="btn-cancel">Cancelar</button>
                    <button class="btn-ok">Aceptar</button>
                </div>
            `;

      document.body.appendChild(dialog);

      dialog.querySelector('.btn-ok').addEventListener('click', () => {
        document.body.removeChild(dialog);
        resolve(true);
      });

      dialog.querySelector('.btn-cancel').addEventListener('click', () => {
        document.body.removeChild(dialog);
        resolve(false);
      });
    });
  }

  static async prompt(message, defaultValue = '', title = 'Entrada') {
    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.className = 'custom-dialog';
      dialog.innerHTML = `
                <div class="dialog-overlay"></div>
                <div class="dialog-content">
                    <h3>${title}</h3>
                    <p>${message}</p>
                    <input type="text" value="${defaultValue}" />
                    <button class="btn-cancel">Cancelar</button>
                    <button class="btn-ok">Aceptar</button>
                </div>
            `;

      document.body.appendChild(dialog);

      const input = dialog.querySelector('input');
      input.focus();

      dialog.querySelector('.btn-ok').addEventListener('click', () => {
        const value = input.value;
        document.body.removeChild(dialog);
        resolve(value);
      });

      dialog.querySelector('.btn-cancel').addEventListener('click', () => {
        document.body.removeChild(dialog);
        resolve(null);
      });

      input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
          const value = input.value;
          document.body.removeChild(dialog);
          resolve(value);
        }
      });
    });
  }
}

// Uso de diálogos personalizados
// await CustomDialog.alert('Operación exitosa', 'Éxito');
// const confirmed = await CustomDialog.confirm('¿Continuar?', 'Confirmación');
// const name = await CustomDialog.prompt('Tu nombre:', 'Anónimo', 'Registro');

Window.location - Control de URLs

// Objeto location contiene información sobre la URL actual
console.log('URL completa:', window.location.href);
console.log('Protocolo:', window.location.protocol); // 'https:'
console.log('Host:', window.location.host); // 'ejemplo.com:443'
console.log('Hostname:', window.location.hostname); // 'ejemplo.com'
console.log('Puerto:', window.location.port); // '443'
console.log('Pathname:', window.location.pathname); // '/blog/articulo'
console.log('Search:', window.location.search); // '?id=123&cat=tech'
console.log('Hash:', window.location.hash); // '#seccion'

// Clase para manipular location de forma segura
class LocationManager {
  // Obtener parámetros de query string
  static getParams() {
    return Object.fromEntries(new URLSearchParams(window.location.search));
  }

  // Obtener parámetro específico
  static getParam(name, defaultValue = null) {
    const params = new URLSearchParams(window.location.search);
    return params.get(name) || defaultValue;
  }

  // Navegar a nueva URL
  static navigate(url) {
    window.location.href = url;
  }

  // Recargar página
  static reload(forceReload = false) {
    window.location.reload(forceReload);
  }

  // Reemplazar URL sin agregar al historial
  static replace(url) {
    window.location.replace(url);
  }

  // Agregar/modificar parámetros de query sin recargar
  static updateParams(params) {
    const url = new URL(window.location.href);

    Object.entries(params).forEach(([key, value]) => {
      if (value === null || value === undefined) {
        url.searchParams.delete(key);
      } else {
        url.searchParams.set(key, value);
      }
    });

    window.history.replaceState({}, '', url.toString());
  }

  // Eliminar parámetro específico
  static removeParam(name) {
    const url = new URL(window.location.href);
    url.searchParams.delete(name);
    window.history.replaceState({}, '', url.toString());
  }

  // Obtener ruta relativa
  static getRelativePath() {
    return window.location.pathname + window.location.search + window.location.hash;
  }

  // Verificar si es HTTPS
  static isSecure() {
    return window.location.protocol === 'https:';
  }

  // Verificar si es localhost
  static isLocalhost() {
    return ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname);
  }
}

// Ejemplos de uso
console.log('Parámetros:', LocationManager.getParams());
console.log('Usuario ID:', LocationManager.getParam('userId', 'guest'));

// Actualizar parámetros sin recargar
LocationManager.updateParams({ page: 2, sort: 'name' });

// Navegar a otra página
// LocationManager.navigate('/dashboard');

// Recargar forzando desde servidor
// LocationManager.reload(true);

Window.history - Navegación por historial

// Navegar por el historial
window.history.back(); // Ir atrás (equivale al botón "Atrás")
window.history.forward(); // Ir adelante
window.history.go(-2); // Ir 2 páginas atrás
window.history.go(1); // Ir 1 página adelante

// Cantidad de páginas en el historial
console.log('Páginas en historial:', window.history.length);

// History API para SPAs (Single Page Applications)
class HistoryManager {
  constructor() {
    this.setupPopstateListener();
  }

  // Agregar nueva entrada al historial
  push(url, state = {}) {
    window.history.pushState(state, '', url);
    this.triggerRouteChange();
  }

  // Reemplazar entrada actual del historial
  replace(url, state = {}) {
    window.history.replaceState(state, '', url);
    this.triggerRouteChange();
  }

  // Obtener estado actual
  getState() {
    return window.history.state;
  }

  // Navegar atrás
  back() {
    window.history.back();
  }

  // Navegar adelante
  forward() {
    window.history.forward();
  }

  // Escuchar cambios de ruta
  setupPopstateListener() {
    window.addEventListener('popstate', (event) => {
      console.log('Estado del historial:', event.state);
      this.triggerRouteChange();
    });
  }

  // Evento personalizado para cambios de ruta
  triggerRouteChange() {
    const event = new CustomEvent('routechange', {
      detail: {
        path: window.location.pathname,
        search: window.location.search,
        hash: window.location.hash,
        state: window.history.state,
      },
    });
    window.dispatchEvent(event);
  }

  // Interceptar clics en enlaces
  interceptLinks() {
    document.addEventListener('click', (event) => {
      const link = event.target.closest('a[href]');

      if (link && this.shouldIntercept(link)) {
        event.preventDefault();
        const href = link.getAttribute('href');
        this.push(href);
      }
    });
  }

  shouldIntercept(link) {
    const href = link.getAttribute('href');

    // No interceptar enlaces externos
    if (href.startsWith('http') && !href.startsWith(window.location.origin)) {
      return false;
    }

    // No interceptar si tiene target
    if (link.target) {
      return false;
    }

    // No interceptar download links
    if (link.hasAttribute('download')) {
      return false;
    }

    return true;
  }
}

// Uso del HistoryManager
const historyManager = new HistoryManager();

// Navegar programáticamente
historyManager.push('/nueva-pagina', { userId: 123 });

// Escuchar cambios de ruta
window.addEventListener('routechange', (event) => {
  console.log('Ruta cambió a:', event.detail.path);
  console.log('Estado:', event.detail.state);
});

// Interceptar todos los enlaces
historyManager.interceptLinks();

Window.navigator - Información del navegador

// Información básica del navegador
console.log('User Agent:', window.navigator.userAgent);
console.log('Lenguaje:', window.navigator.language);
console.log('Lenguajes:', window.navigator.languages);
console.log('Online:', window.navigator.onLine);
console.log('Plataforma:', window.navigator.platform);
console.log('Cookies habilitadas:', window.navigator.cookieEnabled);

// Clase para detectar características del navegador
class BrowserDetector {
  // Detectar navegador específico
  static getBrowser() {
    const ua = navigator.userAgent;

    if (ua.includes('Firefox/')) {
      return 'Firefox';
    } else if (ua.includes('Edg/')) {
      return 'Edge';
    } else if (ua.includes('Chrome/')) {
      return 'Chrome';
    } else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
      return 'Safari';
    } else if (ua.includes('Opera/') || ua.includes('OPR/')) {
      return 'Opera';
    }
    return 'Unknown';
  }

  // Detectar sistema operativo
  static getOS() {
    const ua = navigator.userAgent;
    const platform = navigator.platform;

    if (ua.includes('Win')) return 'Windows';
    if (ua.includes('Mac')) return 'macOS';
    if (ua.includes('Linux')) return 'Linux';
    if (ua.includes('Android')) return 'Android';
    if (ua.includes('iOS') || platform.includes('iPhone') || platform.includes('iPad')) {
      return 'iOS';
    }
    return 'Unknown';
  }

  // Detectar si es dispositivo móvil
  static isMobile() {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
      navigator.userAgent
    );
  }

  // Detectar si es tablet
  static isTablet() {
    const ua = navigator.userAgent;
    return /(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua);
  }

  // Detectar si es desktop
  static isDesktop() {
    return !this.isMobile() && !this.isTablet();
  }

  // Detectar soporte de características
  static supports(feature) {
    const features = {
      touch: 'ontouchstart' in window,
      serviceWorker: 'serviceWorker' in navigator,
      geolocation: 'geolocation' in navigator,
      webgl: !!document.createElement('canvas').getContext('webgl'),
      websocket: 'WebSocket' in window,
      localStorage: (() => {
        try {
          localStorage.setItem('test', 'test');
          localStorage.removeItem('test');
          return true;
        } catch (e) {
          return false;
        }
      })(),
      indexedDB: 'indexedDB' in window,
      notifications: 'Notification' in window,
      camera: 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices,
    };

    return features[feature] || false;
  }

  // Información completa del dispositivo
  static getDeviceInfo() {
    return {
      browser: this.getBrowser(),
      os: this.getOS(),
      deviceType: this.isMobile() ? 'mobile' : this.isTablet() ? 'tablet' : 'desktop',
      language: navigator.language,
      online: navigator.onLine,
      cookiesEnabled: navigator.cookieEnabled,
      screenResolution: `${window.screen.width}x${window.screen.height}`,
      viewportSize: `${window.innerWidth}x${window.innerHeight}`,
      colorDepth: window.screen.colorDepth,
      pixelRatio: window.devicePixelRatio || 1,
    };
  }
}

// Uso del detector
console.log('Navegador:', BrowserDetector.getBrowser());
console.log('Sistema:', BrowserDetector.getOS());
console.log('Es móvil:', BrowserDetector.isMobile());
console.log('Soporta touch:', BrowserDetector.supports('touch'));
console.log('Info completa:', BrowserDetector.getDeviceInfo());

// Detectar estado de conexión
window.addEventListener('online', () => {
  console.log('Conexión restaurada');
});

window.addEventListener('offline', () => {
  console.log('Sin conexión');
});

Geolocalización

// API de geolocalización
class GeolocationManager {
  // Obtener posición actual
  static getCurrentPosition(options = {}) {
    return new Promise((resolve, reject) => {
      if (!navigator.geolocation) {
        reject(new Error('Geolocalización no soportada'));
        return;
      }

      navigator.geolocation.getCurrentPosition(
        (position) => {
          resolve({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            accuracy: position.coords.accuracy,
            altitude: position.coords.altitude,
            altitudeAccuracy: position.coords.altitudeAccuracy,
            heading: position.coords.heading,
            speed: position.coords.speed,
            timestamp: position.timestamp,
          });
        },
        (error) => {
          reject(new Error(this.getErrorMessage(error)));
        },
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 0,
          ...options,
        }
      );
    });
  }

  // Observar cambios de posición
  static watchPosition(callback, errorCallback, options = {}) {
    if (!navigator.geolocation) {
      errorCallback(new Error('Geolocalización no soportada'));
      return null;
    }

    const watchId = navigator.geolocation.watchPosition(
      (position) => {
        callback({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
          timestamp: position.timestamp,
        });
      },
      (error) => {
        errorCallback(new Error(this.getErrorMessage(error)));
      },
      {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0,
        ...options,
      }
    );

    return watchId;
  }

  // Detener observación
  static clearWatch(watchId) {
    if (watchId !== null) {
      navigator.geolocation.clearWatch(watchId);
    }
  }

  // Obtener mensaje de error legible
  static getErrorMessage(error) {
    switch (error.code) {
      case error.PERMISSION_DENIED:
        return 'Permiso denegado por el usuario';
      case error.POSITION_UNAVAILABLE:
        return 'Posición no disponible';
      case error.TIMEOUT:
        return 'Tiempo de espera agotado';
      default:
        return 'Error desconocido';
    }
  }

  // Calcular distancia entre dos puntos (fórmula de Haversine)
  static calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371; // Radio de la Tierra en km
    const dLat = this.toRad(lat2 - lat1);
    const dLon = this.toRad(lon2 - lon1);

    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.toRad(lat1)) *
        Math.cos(this.toRad(lat2)) *
        Math.sin(dLon / 2) *
        Math.sin(dLon / 2);

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c; // Distancia en km
  }

  static toRad(degrees) {
    return (degrees * Math.PI) / 180;
  }
}

// Uso de geolocalización
async function getUserLocation() {
  try {
    const position = await GeolocationManager.getCurrentPosition();
    console.log('Ubicación actual:', position);

    // Calcular distancia a un punto específico
    const distanceToMadrid = GeolocationManager.calculateDistance(
      position.latitude,
      position.longitude,
      40.4168, // Madrid
      -3.7038
    );
    console.log('Distancia a Madrid:', distanceToMadrid.toFixed(2), 'km');
  } catch (error) {
    console.error('Error obteniendo ubicación:', error.message);
  }
}

// Observar cambios de ubicación
// const watchId = GeolocationManager.watchPosition(
//     (position) => console.log('Nueva posición:', position),
//     (error) => console.error('Error:', error.message)
// );

// Detener observación después de 30 segundos
// setTimeout(() => GeolocationManager.clearWatch(watchId), 30000);

Window.screen - Información de pantalla

// Propiedades de la pantalla
console.log('Ancho total:', window.screen.width);
console.log('Alto total:', window.screen.height);
console.log('Ancho disponible:', window.screen.availWidth);
console.log('Alto disponible:', window.screen.availHeight);
console.log('Profundidad de color:', window.screen.colorDepth);
console.log('Profundidad de píxel:', window.screen.pixelDepth);
console.log('Orientación:', window.screen.orientation?.type);

// Clase para gestionar información de pantalla
class ScreenManager {
  // Obtener información completa
  static getInfo() {
    return {
      total: {
        width: screen.width,
        height: screen.height,
      },
      available: {
        width: screen.availWidth,
        height: screen.availHeight,
      },
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight,
      },
      colorDepth: screen.colorDepth,
      pixelRatio: window.devicePixelRatio || 1,
      orientation: screen.orientation?.type || 'unknown',
      touchSupport: 'ontouchstart' in window,
    };
  }

  // Detectar orientación
  static getOrientation() {
    if (screen.orientation) {
      return screen.orientation.type; // 'portrait-primary', 'landscape-primary', etc.
    }

    // Fallback para navegadores sin soporte
    return window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
  }

  // Escuchar cambios de orientación
  static onOrientationChange(callback) {
    if (screen.orientation) {
      screen.orientation.addEventListener('change', () => {
        callback(screen.orientation.type);
      });
    } else {
      // Fallback usando resize
      window.addEventListener('resize', () => {
        callback(this.getOrientation());
      });
    }
  }

  // Verificar si es pantalla retina
  static isRetina() {
    return window.devicePixelRatio > 1;
  }

  // Verificar si está en modo fullscreen
  static isFullscreen() {
    return !!(
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement
    );
  }

  // Entrar en modo fullscreen
  static async enterFullscreen(element = document.documentElement) {
    try {
      if (element.requestFullscreen) {
        await element.requestFullscreen();
      } else if (element.webkitRequestFullscreen) {
        await element.webkitRequestFullscreen();
      } else if (element.mozRequestFullScreen) {
        await element.mozRequestFullScreen();
      } else if (element.msRequestFullscreen) {
        await element.msRequestFullscreen();
      }
      return true;
    } catch (error) {
      console.error('Error entrando en fullscreen:', error);
      return false;
    }
  }

  // Salir de modo fullscreen
  static async exitFullscreen() {
    try {
      if (document.exitFullscreen) {
        await document.exitFullscreen();
      } else if (document.webkitExitFullscreen) {
        await document.webkitExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        await document.mozCancelFullScreen();
      } else if (document.msExitFullscreen) {
        await document.msExitFullscreen();
      }
      return true;
    } catch (error) {
      console.error('Error saliendo de fullscreen:', error);
      return false;
    }
  }

  // Toggle fullscreen
  static async toggleFullscreen(element) {
    if (this.isFullscreen()) {
      return await this.exitFullscreen();
    } else {
      return await this.enterFullscreen(element);
    }
  }
}

// Uso del ScreenManager
console.log('Info de pantalla:', ScreenManager.getInfo());
console.log('Orientación:', ScreenManager.getOrientation());
console.log('Es Retina:', ScreenManager.isRetina());

// Escuchar cambios de orientación
ScreenManager.onOrientationChange((orientation) => {
  console.log('Nueva orientación:', orientation);
});

// Toggle fullscreen en un elemento
// document.querySelector('#video-player').addEventListener('click', async () => {
//     await ScreenManager.toggleFullscreen(document.querySelector('#video-player'));
// });

Timers - Ejecución temporizada

// setTimeout - ejecutar una vez después de un delay
const timeoutId = setTimeout(() => {
  console.log('Ejecutado después de 2 segundos');
}, 2000);

// Cancelar timeout
clearTimeout(timeoutId);

// setInterval - ejecutar repetidamente
const intervalId = setInterval(() => {
  console.log('Ejecutado cada segundo');
}, 1000);

// Cancelar interval
clearInterval(intervalId);

// Clase para gestionar timers de forma avanzada
class TimerManager {
  constructor() {
    this.timers = new Map();
    this.nextId = 0;
  }

  // Timeout con identificador propio
  timeout(callback, delay, ...args) {
    const id = this.nextId++;

    const timeoutId = setTimeout(() => {
      callback(...args);
      this.timers.delete(id);
    }, delay);

    this.timers.set(id, { type: 'timeout', id: timeoutId });
    return id;
  }

  // Interval con identificador propio
  interval(callback, delay, ...args) {
    const id = this.nextId++;

    const intervalId = setInterval(() => {
      callback(...args);
    }, delay);

    this.timers.set(id, { type: 'interval', id: intervalId });
    return id;
  }

  // Cancelar timer específico
  clear(id) {
    const timer = this.timers.get(id);

    if (timer) {
      if (timer.type === 'timeout') {
        clearTimeout(timer.id);
      } else {
        clearInterval(timer.id);
      }
      this.timers.delete(id);
      return true;
    }

    return false;
  }

  // Cancelar todos los timers
  clearAll() {
    for (const [id, timer] of this.timers) {
      if (timer.type === 'timeout') {
        clearTimeout(timer.id);
      } else {
        clearInterval(timer.id);
      }
    }
    this.timers.clear();
  }

  // Debounce - ejecutar solo después de que pasen X ms sin nuevas llamadas
  debounce(callback, delay) {
    let timeoutId = null;

    return (...args) => {
      if (timeoutId) {
        clearTimeout(timeoutId);
      }

      timeoutId = setTimeout(() => {
        callback(...args);
        timeoutId = null;
      }, delay);
    };
  }

  // Throttle - ejecutar como máximo una vez cada X ms
  throttle(callback, delay) {
    let lastCall = 0;
    let timeoutId = null;

    return (...args) => {
      const now = Date.now();
      const timeSinceLastCall = now - lastCall;

      if (timeSinceLastCall >= delay) {
        lastCall = now;
        callback(...args);
      } else {
        if (timeoutId) {
          clearTimeout(timeoutId);
        }

        timeoutId = setTimeout(() => {
          lastCall = Date.now();
          callback(...args);
        }, delay - timeSinceLastCall);
      }
    };
  }

  // Retry - reintentar operación con backoff exponencial
  async retry(operation, maxAttempts = 3, baseDelay = 1000) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (error) {
        if (attempt === maxAttempts) {
          throw error;
        }

        const delay = baseDelay * Math.pow(2, attempt - 1);
        console.log(`Intento ${attempt} falló. Reintentando en ${delay}ms...`);

        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  // Countdown - cuenta regresiva
  countdown(seconds, onTick, onComplete) {
    let remaining = seconds;

    onTick(remaining);

    const intervalId = this.interval(() => {
      remaining--;
      onTick(remaining);

      if (remaining <= 0) {
        this.clear(intervalId);
        if (onComplete) onComplete();
      }
    }, 1000);

    return intervalId;
  }
}

// Uso del TimerManager
const timerManager = new TimerManager();

// Timeout simple
const id1 = timerManager.timeout(() => {
  console.log('Timer ejecutado');
}, 2000);

// Interval que se auto-cancela
let count = 0;
const id2 = timerManager.interval(() => {
  count++;
  console.log('Count:', count);

  if (count >= 5) {
    timerManager.clear(id2);
  }
}, 1000);

// Debounce para búsqueda
const searchInput = document.querySelector('#search');
const debouncedSearch = timerManager.debounce((value) => {
  console.log('Buscando:', value);
  // Realizar búsqueda
}, 500);

// searchInput?.addEventListener('input', (e) => debouncedSearch(e.target.value));

// Throttle para scroll
const throttledScroll = timerManager.throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 200);

// window.addEventListener('scroll', throttledScroll);

// Retry con backoff exponencial
// async function fetchWithRetry() {
//     try {
//         const data = await timerManager.retry(
//             () => fetch('/api/data').then(r => r.json()),
//             3,    // 3 intentos
//             1000  // delay inicial de 1s
//         );
//         console.log('Datos obtenidos:', data);
//     } catch (error) {
//         console.error('Error después de reintentos:', error);
//     }
// }

// Countdown
timerManager.countdown(
  10,
  (remaining) => console.log(`Quedan ${remaining} segundos`),
  () => console.log('¡Tiempo terminado!')
);

Window sizing y scrolling

// Dimensiones de la ventana
console.log('Ancho interno:', window.innerWidth);
console.log('Alto interno:', window.innerHeight);
console.log('Ancho externo:', window.outerWidth);
console.log('Alto externo:', window.outerHeight);

// Posición de scroll
console.log('Scroll X:', window.scrollX || window.pageXOffset);
console.log('Scroll Y:', window.scrollY || window.pageYOffset);

// Clase para gestionar ventana y scroll
class WindowManager {
  // Scroll suave a posición
  static scrollTo(x, y, behavior = 'smooth') {
    window.scrollTo({
      top: y,
      left: x,
      behavior: behavior,
    });
  }

  // Scroll a elemento específico
  static scrollToElement(element, options = {}) {
    if (typeof element === 'string') {
      element = document.querySelector(element);
    }

    if (element) {
      element.scrollIntoView({
        behavior: 'smooth',
        block: 'start',
        inline: 'nearest',
        ...options,
      });
    }
  }

  // Obtener posición de scroll
  static getScrollPosition() {
    return {
      x: window.scrollX || window.pageXOffset,
      y: window.scrollY || window.pageYOffset,
    };
  }

  // Verificar si está al final del scroll
  static isAtBottom(threshold = 100) {
    const scrollTop = window.scrollY || window.pageYOffset;
    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;

    return scrollTop + windowHeight >= documentHeight - threshold;
  }

  // Verificar si está al inicio del scroll
  static isAtTop(threshold = 0) {
    const scrollTop = window.scrollY || window.pageYOffset;
    return scrollTop <= threshold;
  }

  // Obtener porcentaje de scroll
  static getScrollPercentage() {
    const scrollTop = window.scrollY || window.pageYOffset;
    const documentHeight = document.documentElement.scrollHeight;
    const windowHeight = window.innerHeight;

    return (scrollTop / (documentHeight - windowHeight)) * 100;
  }

  // Deshabilitar scroll
  static disableScroll() {
    document.body.style.overflow = 'hidden';
    document.documentElement.style.overflow = 'hidden';
  }

  // Habilitar scroll
  static enableScroll() {
    document.body.style.overflow = '';
    document.documentElement.style.overflow = '';
  }

  // Smooth scroll con callback
  static smoothScrollTo(target, duration = 1000, callback) {
    const start = window.scrollY || window.pageYOffset;
    const distance = target - start;
    const startTime = performance.now();

    function animation(currentTime) {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);

      // Easing function (ease in out cubic)
      const easeInOutCubic =
        progress < 0.5
          ? 4 * progress * progress * progress
          : 1 - Math.pow(-2 * progress + 2, 3) / 2;

      window.scrollTo(0, start + distance * easeInOutCubic);

      if (progress < 1) {
        requestAnimationFrame(animation);
      } else if (callback) {
        callback();
      }
    }

    requestAnimationFrame(animation);
  }

  // Redimensionar ventana (solo funciona en ventanas abiertas con window.open)
  static resizeWindow(width, height) {
    window.resizeTo(width, height);
  }

  // Mover ventana
  static moveWindow(x, y) {
    window.moveTo(x, y);
  }

  // Abrir nueva ventana con configuración
  static openWindow(url, name, options = {}) {
    const defaultOptions = {
      width: 800,
      height: 600,
      left: screen.width / 2 - 400,
      top: screen.height / 2 - 300,
      toolbar: 'no',
      menubar: 'no',
      scrollbars: 'yes',
      resizable: 'yes',
    };

    const finalOptions = { ...defaultOptions, ...options };

    const features = Object.entries(finalOptions)
      .map(([key, value]) => `${key}=${value}`)
      .join(',');

    return window.open(url, name, features);
  }
}

// Uso del WindowManager
console.log('Posición de scroll:', WindowManager.getScrollPosition());
console.log('Porcentaje scrolleado:', WindowManager.getScrollPercentage().toFixed(2) + '%');

// Scroll suave al top
// WindowManager.scrollTo(0, 0);

// Scroll a elemento
// WindowManager.scrollToElement('#section-2');

// Detectar cuando llega al final
window.addEventListener('scroll', () => {
  if (WindowManager.isAtBottom()) {
    console.log('¡Llegaste al final!');
    // Cargar más contenido
  }
});

// Abrir ventana popup centrada
// const popup = WindowManager.openWindow(
//     '/login',
//     'LoginWindow',
//     { width: 400, height: 500 }
// );

Storage - Almacenamiento local

// LocalStorage - persiste incluso después de cerrar el navegador
localStorage.setItem('usuario', 'Juan');
const usuario = localStorage.getItem('usuario');
localStorage.removeItem('usuario');
localStorage.clear();

// SessionStorage - se borra al cerrar la pestaña
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');

// Clase para gestionar storage de forma avanzada
class StorageManager {
  constructor(storage = localStorage) {
    this.storage = storage;
  }

  // Guardar con serialización automática
  set(key, value, expirationMinutes = null) {
    const item = {
      value: value,
      timestamp: Date.now(),
      expiration: expirationMinutes ? Date.now() + expirationMinutes * 60 * 1000 : null,
    };

    try {
      this.storage.setItem(key, JSON.stringify(item));
      return true;
    } catch (error) {
      console.error('Error guardando en storage:', error);
      return false;
    }
  }

  // Obtener con deserialización automática
  get(key, defaultValue = null) {
    try {
      const itemStr = this.storage.getItem(key);

      if (!itemStr) {
        return defaultValue;
      }

      const item = JSON.parse(itemStr);

      // Verificar expiración
      if (item.expiration && Date.now() > item.expiration) {
        this.remove(key);
        return defaultValue;
      }

      return item.value;
    } catch (error) {
      console.error('Error obteniendo de storage:', error);
      return defaultValue;
    }
  }

  // Remover item
  remove(key) {
    this.storage.removeItem(key);
  }

  // Limpiar todo
  clear() {
    this.storage.clear();
  }

  // Obtener todas las claves
  keys() {
    return Object.keys(this.storage);
  }

  // Verificar si existe
  has(key) {
    return this.storage.getItem(key) !== null;
  }

  // Obtener tamaño usado
  getSize() {
    let size = 0;
    for (let key in this.storage) {
      if (this.storage.hasOwnProperty(key)) {
        size += this.storage[key].length + key.length;
      }
    }
    return size; // bytes
  }

  // Limpiar items expirados
  clearExpired() {
    const keys = this.keys();
    let cleared = 0;

    keys.forEach((key) => {
      try {
        const itemStr = this.storage.getItem(key);
        const item = JSON.parse(itemStr);

        if (item.expiration && Date.now() > item.expiration) {
          this.remove(key);
          cleared++;
        }
      } catch (error) {
        // Item inválido, ignorar
      }
    });

    return cleared;
  }

  // Observar cambios
  onChange(callback) {
    window.addEventListener('storage', (event) => {
      if (event.storageArea === this.storage) {
        callback({
          key: event.key,
          oldValue: event.oldValue,
          newValue: event.newValue,
          url: event.url,
        });
      }
    });
  }
}

// Uso del StorageManager
const storage = new StorageManager(localStorage);

// Guardar datos con expiración de 30 minutos
storage.set('userSession', { id: 123, name: 'Juan' }, 30);

// Obtener datos
const session = storage.get('userSession');
console.log('Sesión:', session);

// Guardar array
storage.set('favoritos', ['item1', 'item2', 'item3']);

// Obtener con valor por defecto
const config = storage.get('appConfig', { theme: 'light', lang: 'es' });

// Verificar tamaño usado
console.log('Storage usado:', (storage.getSize() / 1024).toFixed(2), 'KB');

// Limpiar items expirados
const cleared = storage.clearExpired();
console.log('Items expirados eliminados:', cleared);

// Observar cambios (útil para sincronizar entre pestañas)
storage.onChange((change) => {
  console.log('Storage cambió:', change);
});

Performance API

// Medir performance
console.log('Tiempo desde navegación:', performance.now());
console.log('Timing de navegación:', performance.timing);
console.log('Entradas de performance:', performance.getEntries());

// Clase para medir performance
class PerformanceMonitor {
  // Marcar punto en el tiempo
  static mark(name) {
    performance.mark(name);
  }

  // Medir entre dos marcas
  static measure(name, startMark, endMark) {
    try {
      performance.measure(name, startMark, endMark);
      const measure = performance.getEntriesByName(name)[0];
      return measure.duration;
    } catch (error) {
      console.error('Error midiendo performance:', error);
      return null;
    }
  }

  // Limpiar marcas y medidas
  static clear(name = null) {
    if (name) {
      performance.clearMarks(name);
      performance.clearMeasures(name);
    } else {
      performance.clearMarks();
      performance.clearMeasures();
    }
  }

  // Medir tiempo de ejecución de función
  static async measureFunction(fn, name = 'function-execution') {
    const startMark = `${name}-start`;
    const endMark = `${name}-end`;

    this.mark(startMark);
    const result = await fn();
    this.mark(endMark);

    const duration = this.measure(name, startMark, endMark);

    console.log(`${name} tomó ${duration?.toFixed(2)}ms`);

    return result;
  }

  // Obtener métricas de navegación
  static getNavigationMetrics() {
    const timing = performance.timing;

    return {
      // Tiempo total de carga
      totalLoadTime: timing.loadEventEnd - timing.navigationStart,

      // Tiempo de respuesta del servidor
      serverResponseTime: timing.responseEnd - timing.requestStart,

      // Tiempo de renderizado DOM
      domRenderTime: timing.domComplete - timing.domLoading,

      // Tiempo hasta DOM interactivo
      domInteractive: timing.domInteractive - timing.navigationStart,

      // Tiempo hasta DOM completo
      domComplete: timing.domComplete - timing.navigationStart,

      // Tiempo de descarga de página
      pageDownloadTime: timing.responseEnd - timing.responseStart,

      // Tiempo de redirección
      redirectTime: timing.redirectEnd - timing.redirectStart,

      // Tiempo de DNS lookup
      dnsLookupTime: timing.domainLookupEnd - timing.domainLookupStart,

      // Tiempo de conexión TCP
      tcpConnectionTime: timing.connectEnd - timing.connectStart,
    };
  }

  // Obtener métricas de recursos
  static getResourceMetrics() {
    const resources = performance.getEntriesByType('resource');

    return resources.map((resource) => ({
      name: resource.name,
      type: resource.initiatorType,
      duration: resource.duration,
      size: resource.transferSize,
      startTime: resource.startTime,
    }));
  }

  // Observer para nuevas entradas de performance
  static observePerformance(callback) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        callback(entry);
      }
    });

    observer.observe({
      entryTypes: ['measure', 'navigation', 'resource', 'paint'],
    });

    return observer;
  }
}

// Uso del PerformanceMonitor
console.log('Métricas de navegación:', PerformanceMonitor.getNavigationMetrics());

// Medir operación
PerformanceMonitor.mark('operacion-start');

// ... código a medir ...

PerformanceMonitor.mark('operacion-end');
const duration = PerformanceMonitor.measure('mi-operacion', 'operacion-start', 'operacion-end');
console.log('Operación tomó:', duration, 'ms');

// Medir función
// await PerformanceMonitor.measureFunction(async () => {
//     const data = await fetch('/api/data');
//     return await data.json();
// }, 'fetch-data');

// Observar performance
const observer = PerformanceMonitor.observePerformance((entry) => {
  console.log('Nueva entrada de performance:', entry);
});

Conclusión

El BOM es tu interfaz con el navegador - mientras el DOM te da control sobre el contenido de la página, el BOM te da control sobre el navegador mismo. Juntos forman el arsenal completo para crear aplicaciones web verdaderamente interactivas.

Principios clave para usar el BOM efectivamente:

  1. Usa las APIs modernas - History API en lugar de manipular location.hash
  2. Respeta la privacidad - Solicita permisos para geolocalización y notificaciones
  3. Maneja errores gracefully - No todos los navegadores soportan todas las APIs
  4. Optimiza el almacenamiento - localStorage tiene límites, usa con moderación
  5. Mide el performance - Lo que no se mide, no se puede optimizar

El BOM moderno es potente - desde detectar si el usuario está online, hasta medir el performance milisegundo a milisegundo. Pero con gran poder viene gran responsabilidad: usa estas APIs para mejorar la experiencia del usuario, no para rastrearlo o molestarlo.

Recuerda: El BOM varía entre navegadores más que el DOM. Siempre verifica compatibilidad y proporciona fallbacks cuando sea necesario. La detección de características es tu amiga.

El BOM es el puente entre tu código y el mundo exterior - úsalo sabiamente para crear experiencias web que se sientan nativas, responsivas y respetuosas con el usuario.