Detect Browser Tab Change in JavaScript: Boost Engagement & Control - Techvblogs

Detect Browser Tab Change in JavaScript: Boost Engagement & Control

Learn how to detect browser tab change in JavaScript and improve user focus and session control with real examples.


Suresh Ramani - Author - Techvblogs
Suresh Ramani
 

21 hours ago

TechvBlogs - Google News

Managing user attention in today’s multi-tab browsing environment presents unique challenges for web developers. When users switch between tabs, scroll through social media, or multitask across applications, your carefully crafted web experience can lose momentum, waste resources, or miss critical engagement opportunities.

JavaScript’s Page Visibility API provides powerful tools to detect when users switch tabs, allowing you to pause videos, save user progress, manage real-time connections, and create more responsive applications. Whether you’re building a video streaming platform that needs to pause playback when users switch tabs, an e-learning system that tracks engagement, or a financial application requiring enhanced security measures, understanding tab visibility detection is essential for modern web development.

This comprehensive guide will teach you practical techniques for implementing tab change detection, from basic visibility state monitoring to advanced security controls and performance optimizations that create seamless user experiences while respecting user privacy and browser limitations.

Why Tracking Browser Tab Changes Matters in Web Development

Modern web users operate in a multi-tab, multi-application environment where attention spans are fragmented and resource management becomes critical. Understanding when your application has user focus enables intelligent behavior that improves performance, enhances security, and creates more intuitive user experiences.

Performance Benefits:

  • Resource conservation: Pause expensive operations like animations, video playback, or data fetching when tabs are inactive
  • Battery optimization: Reduce CPU and GPU usage on mobile devices by limiting background processing
  • Bandwidth management: Control real-time data synchronization based on user engagement

User Experience Improvements:

  • Seamless media playback: Automatically pause and resume content based on user attention
  • Progress preservation: Save user input and form data before potential tab closure
  • Contextual notifications: Display relevant messages when users return to your application

Security and Compliance:

  • Session management: Implement automatic logout for sensitive applications during prolonged inactivity
  • Data protection: Hide or mask sensitive information when tabs lose focus
  • Audit trails: Track user engagement for compliance and analytics purposes

Real-World Use Cases: From User Engagement to Security

Media and Entertainment Platforms: Netflix, YouTube, and Spotify use tab visibility detection to pause playback when users switch tabs, preserving user context and reducing unnecessary bandwidth consumption. This creates a more intuitive experience where content doesn’t play in background tabs.

E-Learning and Training Systems: Educational platforms like Coursera and Khan Academy monitor tab visibility to ensure students remain engaged during lessons, pausing video content and tracking attention for completion certificates and progress analytics.

Financial and Banking Applications: Online banking systems implement automatic logout mechanisms when users switch tabs for extended periods, protecting sensitive financial data from unauthorized access in shared computing environments.

Real-Time Collaboration Tools: Applications like Google Docs, Slack, and Zoom adjust their real-time synchronization behavior based on tab visibility, reducing server load while maintaining responsiveness for active users.

Gaming and Interactive Experiences: Browser-based games pause gameplay mechanics, stop timer functions, and reduce resource consumption when users switch tabs, preventing unfair advantages and conserving device resources.

Understanding the Page Visibility API

What Is the Page Visibility API and How It Works

The Page Visibility API provides a standardized way to determine when a webpage is visible or hidden from the user’s perspective. Unlike traditional focus and blur events that only detect when the entire browser window gains or loses focus, the Page Visibility API specifically tracks tab-level visibility states.

Core API Components:

document.hidden Property: A boolean value indicating whether the page is currently hidden from view:

  • true: Tab is not visible (user switched to another tab, minimized browser, or locked screen)
  • false: Tab is currently visible and has user attention

document.visibilityState Property: Provides more detailed information about the page’s visibility state:

  • "visible": Page is the active tab in a non-minimized browser window
  • "hidden": Page is not visible (tab is inactive, browser minimized, or screen locked)
  • "prerender": Page is being prerendered and not visible to user (rarely used)

visibilitychange Event: Fires whenever the page’s visibility state changes, allowing you to respond immediately to tab switches.

Supported Browsers and Compatibility Notes

The Page Visibility API enjoys excellent browser support across modern browsers:

Browser Version Notes
Chrome 14+ Full support since 2011
Firefox 18+ Complete implementation
Safari 7+ Full support on desktop and mobile
Edge 12+ Native support in all versions
Internet Explorer 10+ Limited support with vendor prefixes

Compatibility Considerations:

// Feature detection with fallback
function hasPageVisibilitySupport() {
  return typeof document.hidden !== 'undefined' || 
         typeof document.webkitHidden !== 'undefined' || 
         typeof document.mozHidden !== 'undefined' || 
         typeof document.msHidden !== 'undefined';
}

// Cross-browser property access
function getVisibilityProps() {
  if (typeof document.hidden !== 'undefined') {
    return {
      hidden: 'hidden',
      visibilityState: 'visibilityState',
      visibilityChange: 'visibilitychange'
    };
  } else if (typeof document.webkitHidden !== 'undefined') {
    return {
      hidden: 'webkitHidden',
      visibilityState: 'webkitVisibilityState',
      visibilityChange: 'webkitvisibilitychange'
    };
  } else if (typeof document.mozHidden !== 'undefined') {
    return {
      hidden: 'mozHidden',
      visibilityState: 'mozVisibilityState',
      visibilityChange: 'mozvisibilitychange'
    };
  } else if (typeof document.msHidden !== 'undefined') {
    return {
      hidden: 'msHidden',
      visibilityState: 'msVisibilityState',
      visibilityChange: 'msvisibilitychange'
    };
  }
  return null;
}

Detecting When a Tab Is Hidden or Visible

Using document.hidden and visibilitychange Effectively

The most straightforward approach to tab visibility detection combines the document.hidden property with the visibilitychange event listener:

// Basic tab visibility detection
function initializeTabVisibilityDetection() {
  // Check initial state
  console.log('Page initially hidden:', document.hidden);
  console.log('Initial visibility state:', document.visibilityState);
  
  // Listen for visibility changes
  document.addEventListener('visibilitychange', handleVisibilityChange);
}

function handleVisibilityChange() {
  if (document.hidden) {
    onTabHidden();
  } else {
    onTabVisible();
  }
}

function onTabHidden() {
  console.log('Tab is now hidden');
  // Implement tab hidden logic
  pauseActivities();
  trackTabLeave();
}

function onTabVisible() {
  console.log('Tab is now visible');
  // Implement tab visible logic
  resumeActivities();
  trackTabReturn();
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', initializeTabVisibilityDetection);

Simple Code Example to Detect Tab Visibility State Changes

Complete Tab Detection System:

class TabVisibilityManager {
  constructor() {
    this.isHidden = document.hidden;
    this.visibilityProps = this.getVisibilityProperties();
    this.callbacks = {
      hidden: [],
      visible: []
    };
    this.timeHidden = 0;
    this.timeVisible = 0;
    this.lastStateChange = Date.now();
    
    this.init();
  }

  getVisibilityProperties() {
    const props = {
      hidden: 'hidden',
      visibilityState: 'visibilityState',
      visibilityChange: 'visibilitychange'
    };

    // Handle vendor prefixes for older browsers
    if (typeof document.hidden === 'undefined') {
      const prefixes = ['webkit', 'moz', 'ms'];
      for (const prefix of prefixes) {
        const hiddenProp = `${prefix}Hidden`;
        if (typeof document[hiddenProp] !== 'undefined') {
          props.hidden = hiddenProp;
          props.visibilityState = `${prefix}VisibilityState`;
          props.visibilityChange = `${prefix}visibilitychange`;
          break;
        }
      }
    }
    
    return props;
  }

  init() {
    if (!this.visibilityProps) {
      console.warn('Page Visibility API not supported');
      return;
    }

    document.addEventListener(
      this.visibilityProps.visibilityChange, 
      this.handleVisibilityChange.bind(this)
    );

    // Track initial state
    this.lastStateChange = Date.now();
  }

  handleVisibilityChange() {
    const now = Date.now();
    const timeDelta = now - this.lastStateChange;

    // Update time tracking
    if (this.isHidden) {
      this.timeHidden += timeDelta;
    } else {
      this.timeVisible += timeDelta;
    }

    // Update current state
    this.isHidden = document[this.visibilityProps.hidden];
    this.lastStateChange = now;

    // Fire appropriate callbacks
    const eventType = this.isHidden ? 'hidden' : 'visible';
    this.fireCallbacks(eventType, {
      isHidden: this.isHidden,
      visibilityState: document[this.visibilityProps.visibilityState],
      timeInPreviousState: timeDelta,
      totalTimeHidden: this.timeHidden,
      totalTimeVisible: this.timeVisible,
      timestamp: now
    });
  }

  onHidden(callback) {
    this.callbacks.hidden.push(callback);
    return this; // Allow chaining
  }

  onVisible(callback) {
    this.callbacks.visible.push(callback);
    return this; // Allow chaining
  }

  fireCallbacks(eventType, data) {
    this.callbacks[eventType].forEach(callback => {
      try {
        callback(data);
      } catch (error) {
        console.error('Error in visibility callback:', error);
      }
    });
  }

  // Utility methods
  getCurrentState() {
    return {
      isHidden: this.isHidden,
      visibilityState: document[this.visibilityProps.visibilityState],
      timeHidden: this.timeHidden,
      timeVisible: this.timeVisible
    };
  }

  getTimeInCurrentState() {
    return Date.now() - this.lastStateChange;
  }
}

// Usage example
const tabManager = new TabVisibilityManager();

tabManager
  .onHidden((data) => {
    console.log('Tab hidden after', data.timeInPreviousState, 'ms of visibility');
    // Pause video, save draft, reduce polling frequency
  })
  .onVisible((data) => {
    console.log('Tab visible after', data.timeInPreviousState, 'ms hidden');
    // Resume video, refresh data, restore normal operations
  });

Enhanced Detection with Debouncing:

class DebouncedTabVisibilityManager extends TabVisibilityManager {
  constructor(debounceDelay = 100) {
    super();
    this.debounceDelay = debounceDelay;
    this.debounceTimer = null;
  }

  handleVisibilityChange() {
    // Clear existing timer
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }

    // Debounce rapid changes
    this.debounceTimer = setTimeout(() => {
      super.handleVisibilityChange();
    }, this.debounceDelay);
  }

  // Immediate state check without debouncing
  getImmediateState() {
    return document[this.visibilityProps.hidden];
  }
}

Enhancing User Engagement with Visibility Detection

Pausing Media or Animations When User Leaves the Tab

Media content should respond intelligently to user attention to create better experiences and conserve resources:

class MediaVisibilityController {
  constructor() {
    this.mediaElements = new Set();
    this.animationIds = new Set();
    this.intervals = new Set();
    this.tabManager = new TabVisibilityManager();
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden(() => this.pauseAllMedia())
      .onVisible(() => this.resumeAllMedia());

    // Auto-detect media elements
    this.detectMediaElements();
    
    // Watch for dynamically added media
    this.setupMediaObserver();
  }

  detectMediaElements() {
    // Find all video and audio elements
    const mediaElements = document.querySelectorAll('video, audio');
    mediaElements.forEach(element => this.registerMediaElement(element));
  }

  setupMediaObserver() {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            const mediaElements = node.querySelectorAll ? 
              node.querySelectorAll('video, audio') : 
              (node.matches && node.matches('video, audio') ? [node] : []);
            
            mediaElements.forEach(element => this.registerMediaElement(element));
          }
        });
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  registerMediaElement(element) {
    if (!this.mediaElements.has(element)) {
      this.mediaElements.add(element);
      
      // Store original autoplay state
      element.dataset.originalAutoplay = element.autoplay;
      
      // Add play/pause tracking
      element.addEventListener('play', () => {
        element.dataset.wasPlayingBeforeHidden = 'true';
      });
      
      element.addEventListener('pause', () => {
        element.dataset.wasPlayingBeforeHidden = 'false';
      });
    }
  }

  pauseAllMedia() {
    this.mediaElements.forEach(element => {
      if (!element.paused && !element.ended) {
        element.dataset.wasPlayingBeforeHidden = 'true';
        element.pause();
        console.log('Paused media element:', element.src || element.currentSrc);
      }
    });

    this.pauseAnimations();
    this.pauseIntervals();
  }

  resumeAllMedia() {
    this.mediaElements.forEach(element => {
      if (element.dataset.wasPlayingBeforeHidden === 'true') {
        element.play().catch(error => {
          console.warn('Could not resume media playback:', error);
        });
        console.log('Resumed media element:', element.src || element.currentSrc);
      }
    });

    this.resumeAnimations();
    this.resumeIntervals();
  }

  // Animation control methods
  registerAnimation(animationId) {
    this.animationIds.add(animationId);
  }

  registerInterval(intervalId) {
    this.intervals.add(intervalId);
  }

  pauseAnimations() {
    // Cancel all registered animation frames
    this.animationIds.forEach(id => {
      cancelAnimationFrame(id);
    });
  }

  resumeAnimations() {
    // Emit custom event for animations to restart
    document.dispatchEvent(new CustomEvent('resumeAnimations'));
  }

  pauseIntervals() {
    // Store interval data and clear them
    this.intervals.forEach(intervalId => {
      clearInterval(intervalId);
    });
  }

  resumeIntervals() {
    // Emit custom event for intervals to restart
    document.dispatchEvent(new CustomEvent('resumeIntervals'));
  }
}

// Usage with existing media elements
const mediaController = new MediaVisibilityController();

// Register custom animations
function startCustomAnimation() {
  function animate() {
    // Animation logic here
    const animationId = requestAnimationFrame(animate);
    mediaController.registerAnimation(animationId);
  }
  animate();
}

// Listen for resume events
document.addEventListener('resumeAnimations', () => {
  startCustomAnimation();
});

Sending Reminder Notifications When the User Returns

Create engaging re-engagement experiences when users return to your tab:

class TabReturnNotificationSystem {
  constructor() {
    this.tabManager = new TabVisibilityManager();
    this.notifications = [];
    this.returnCallbacks = [];
    this.hiddenStartTime = null;
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden((data) => {
        this.hiddenStartTime = data.timestamp;
        this.scheduleNotifications();
      })
      .onVisible((data) => {
        this.handleTabReturn(data);
        this.clearScheduledNotifications();
      });
  }

  scheduleNotifications() {
    // Clear any existing notifications
    this.clearScheduledNotifications();

    // Schedule different notifications based on time away
    const schedules = [
      { delay: 30000, message: "Come back! You have unsaved changes." },
      { delay: 300000, message: "Still there? Your session will expire soon." },
      { delay: 900000, message: "We miss you! Click to return to your work." }
    ];

    schedules.forEach(({ delay, message }) => {
      const timeoutId = setTimeout(() => {
        this.showBrowserNotification(message);
      }, delay);
      
      this.notifications.push(timeoutId);
    });
  }

  clearScheduledNotifications() {
    this.notifications.forEach(timeoutId => clearTimeout(timeoutId));
    this.notifications = [];
  }

  async showBrowserNotification(message) {
    // Request permission if not granted
    if (Notification.permission === 'default') {
      await Notification.requestPermission();
    }

    if (Notification.permission === 'granted') {
      const notification = new Notification('Return to Application', {
        body: message,
        icon: '/favicon.ico',
        tag: 'tab-return-reminder',
        requireInteraction: true
      });

      notification.onclick = () => {
        window.focus();
        notification.close();
      };

      // Auto-close after 10 seconds
      setTimeout(() => notification.close(), 10000);
    }
  }

  handleTabReturn(data) {
    const timeAway = data.timeInPreviousState;
    
    // Show different messages based on time away
    if (timeAway > 30000) { // 30 seconds
      this.showWelcomeBackMessage(timeAway);
    }

    // Fire custom return callbacks
    this.returnCallbacks.forEach(callback => {
      try {
        callback(timeAway, data);
      } catch (error) {
        console.error('Error in return callback:', error);
      }
    });
  }

  showWelcomeBackMessage(timeAway) {
    const minutes = Math.floor(timeAway / 60000);
    const seconds = Math.floor((timeAway % 60000) / 1000);
    
    let message;
    if (minutes > 0) {
      message = `Welcome back! You were away for ${minutes} minute${minutes > 1 ? 's' : ''}.`;
    } else {
      message = `Welcome back! You were away for ${seconds} second${seconds > 1 ? 's' : ''}.`;
    }

    this.createWelcomeBackBanner(message);
  }

  createWelcomeBackBanner(message) {
    // Remove any existing banner
    const existingBanner = document.getElementById('welcome-back-banner');
    if (existingBanner) {
      existingBanner.remove();
    }

    // Create new banner
    const banner = document.createElement('div');
    banner.id = 'welcome-back-banner';
    banner.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      background: linear-gradient(90deg, #4CAF50, #45a049);
      color: white;
      padding: 12px 20px;
      text-align: center;
      z-index: 10000;
      font-family: Arial, sans-serif;
      font-size: 14px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      transform: translateY(-100%);
      transition: transform 0.3s ease-in-out;
    `;

    banner.innerHTML = `
      <span>${message}</span>
      <button onclick="this.parentElement.remove()" style="
        background: rgba(255,255,255,0.2);
        border: none;
        color: white;
        padding: 4px 8px;
        margin-left: 15px;
        border-radius: 3px;
        cursor: pointer;
      ">×</button>
    `;

    document.body.appendChild(banner);

    // Animate in
    setTimeout(() => {
      banner.style.transform = 'translateY(0)';
    }, 100);

    // Auto-remove after 5 seconds
    setTimeout(() => {
      if (banner.parentElement) {
        banner.style.transform = 'translateY(-100%)';
        setTimeout(() => banner.remove(), 300);
      }
    }, 5000);
  }

  onReturn(callback) {
    this.returnCallbacks.push(callback);
    return this;
  }
}

// Usage example
const notificationSystem = new TabReturnNotificationSystem();

notificationSystem.onReturn((timeAway, data) => {
  console.log(`User returned after ${timeAway}ms`);
  
  // Refresh data if user was away for more than 5 minutes
  if (timeAway > 300000) {
    refreshApplicationData();
  }
});

Tracking Active and Inactive Time for Analytics

Implement comprehensive time tracking for user engagement analytics:

class UserEngagementTracker {
  constructor() {
    this.tabManager = new TabVisibilityManager();
    this.sessionData = {
      sessionStart: Date.now(),
      totalActiveTime: 0,
      totalInactiveTime: 0,
      tabSwitches: 0,
      engagementEvents: [],
      currentSessionVisible: !document.hidden
    };
    
    this.heartbeatInterval = null;
    this.analyticsEndpoint = '/api/analytics/engagement';
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden((data) => {
        this.recordTabSwitch('hidden', data);
        this.pauseHeartbeat();
      })
      .onVisible((data) => {
        this.recordTabSwitch('visible', data);
        this.startHeartbeat();
      });

    // Start heartbeat if initially visible
    if (!document.hidden) {
      this.startHeartbeat();
    }

    // Track session end
    window.addEventListener('beforeunload', () => {
      this.endSession();
    });

    // Send periodic updates
    setInterval(() => this.sendAnalytics(), 60000); // Every minute
  }

  recordTabSwitch(state, data) {
    this.sessionData.tabSwitches++;
    
    const event = {
      type: 'tab_switch',
      state: state,
      timestamp: data.timestamp,
      timeInPreviousState: data.timeInPreviousState,
      totalActiveTime: data.totalTimeVisible,
      totalInactiveTime: data.totalTimeHidden
    };

    this.sessionData.engagementEvents.push(event);
    this.sessionData.totalActiveTime = data.totalTimeVisible;
    this.sessionData.totalInactiveTime = data.totalTimeHidden;

    console.log(`Tab ${state}:`, {
      timeInPreviousState: data.timeInPreviousState,
      totalActiveTime: this.sessionData.totalActiveTime,
      totalInactiveTime: this.sessionData.totalInactiveTime
    });
  }

  startHeartbeat() {
    if (this.heartbeatInterval) return;
    
    this.heartbeatInterval = setInterval(() => {
      this.recordHeartbeat();
    }, 30000); // Every 30 seconds
  }

  pauseHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  recordHeartbeat() {
    const event = {
      type: 'heartbeat',
      timestamp: Date.now(),
      url: window.location.href,
      scrollPosition: window.pageYOffset,
      activeElement: document.activeElement.tagName
    };

    this.sessionData.engagementEvents.push(event);
  }

  // Track specific user interactions
  trackInteraction(type, details = {}) {
    const event = {
      type: 'interaction',
      subtype: type,
      timestamp: Date.now(),
      details: details,
      isTabVisible: !document.hidden
    };

    this.sessionData.engagementEvents.push(event);
  }

  getEngagementMetrics() {
    const currentTime = Date.now();
    const sessionDuration = currentTime - this.sessionData.sessionStart;
    const tabData = this.tabManager.getCurrentState();
    
    return {
      sessionDuration: sessionDuration,
      activeTime: tabData.timeVisible,
      inactiveTime: tabData.timeHidden,
      engagementRate: (tabData.timeVisible / sessionDuration) * 100,
      tabSwitches: this.sessionData.tabSwitches,
      eventsCount: this.sessionData.engagementEvents.length,
      averageActiveSessionLength: this.calculateAverageActiveSession(),
      interactionDensity: this.calculateInteractionDensity()
    };
  }

  calculateAverageActiveSession() {
    const activeSessions = this.sessionData.engagementEvents
      .filter(event => event.type === 'tab_switch' && event.state === 'hidden')
      .map(event => event.timeInPreviousState);
    
    if (activeSessions.length === 0) return 0;
    
    const total = activeSessions.reduce((sum, duration) => sum + duration, 0);
    return total / activeSessions.length;
  }

  calculateInteractionDensity() {
    const interactions = this.sessionData.engagementEvents
      .filter(event => event.type === 'interaction');
    
    const activeTime = this.tabManager.getCurrentState().timeVisible;
    return activeTime > 0 ? (interactions.length / activeTime) * 60000 : 0; // Per minute
  }

  async sendAnalytics() {
    const metrics = this.getEngagementMetrics();
    
    try {
      await fetch(this.analyticsEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          sessionId: this.generateSessionId(),
          metrics: metrics,
          events: this.sessionData.engagementEvents.slice(-10) // Last 10 events
        })
      });

      // Clear old events to prevent memory issues
      if (this.sessionData.engagementEvents.length > 100) {
        this.sessionData.engagementEvents = 
          this.sessionData.engagementEvents.slice(-50);
      }
    } catch (error) {
      console.error('Failed to send analytics:', error);
    }
  }

  endSession() {
    const finalMetrics = this.getEngagementMetrics();
    
    // Use sendBeacon for reliable data transmission on page unload
    if (navigator.sendBeacon) {
      navigator.sendBeacon(
        this.analyticsEndpoint,
        JSON.stringify({
          sessionId: this.generateSessionId(),
          sessionEnd: true,
          finalMetrics: finalMetrics,
          events: this.sessionData.engagementEvents
        })
      );
    }
  }

  generateSessionId() {
    if (!this.sessionId) {
      this.sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    }
    return this.sessionId;
  }
}

// Usage with interaction tracking
const engagementTracker = new UserEngagementTracker();

// Track various user interactions
document.addEventListener('click', (e) => {
  engagementTracker.trackInteraction('click', {
    element: e.target.tagName,
    className: e.target.className,
    coordinates: { x: e.clientX, y: e.clientY }
  });
});

document.addEventListener('scroll', () => {
  engagementTracker.trackInteraction('scroll', {
    scrollPosition: window.pageYOffset,
    scrollPercentage: (window.pageYOffset / (document.body.scrollHeight - window.innerHeight)) * 100
  });
});

// Track form interactions
document.addEventListener('input', (e) => {
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
    engagementTracker.trackInteraction('form_input', {
      fieldType: e.target.type,
      fieldName: e.target.name,
      valueLength: e.target.value.length
    });
  }
});

Security and Session Control Based on Tab Visibility

Auto-Logout Users After Inactivity

Implement secure session management that respects user behavior while protecting sensitive data:

class SecurityVisibilityManager {
  constructor(options = {}) {
    this.options = {
      inactivityTimeout: options.inactivityTimeout || 900000, // 15 minutes
      warningTime: options.warningTime || 120000, // 2 minutes before logout
      sensitiveDataTimeout: options.sensitiveDataTimeout || 300000, // 5 minutes
      maxHiddenTime: options.maxHiddenTime || 1800000, // 30 minutes
      autoSaveInterval: options.autoSaveInterval || 30000, // 30 seconds
      ...options
    };

    this.tabManager = new TabVisibilityManager();
    this.lastActivity = Date.now();
    this.inactivityTimer = null;
    this.warningTimer = null;
    this.hiddenTimer = null;
    this.autoSaveTimer = null;
    this.isWarningShown = false;
    
    this.init();
  }

  init() {
    // Track tab visibility changes
    this.tabManager
      .onHidden((data) => {
        this.handleTabHidden(data);
      })
      .onVisible((data) => {
        this.handleTabVisible(data);
      });

    // Track user activity
    this.setupActivityTracking();
    
    // Start initial timers
    this.resetInactivityTimer();
    this.startAutoSave();
  }

  setupActivityTracking() {
    const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
    
    activityEvents.forEach(event => {
      document.addEventListener(event, () => {
        this.recordActivity();
      }, { passive: true });
    });

    // Throttle activity recording
    this.recordActivity = this.throttle(this.recordActivity.bind(this), 1000);
  }

  recordActivity() {
    this.lastActivity = Date.now();
    this.resetInactivityTimer();
    this.hideWarning();
  }

  handleTabHidden(data) {
    console.log('Tab hidden - starting hidden timer');
    
    // Start timer for maximum hidden time
    this.hiddenTimer = setTimeout(() => {
      this.forceLogout('Maximum hidden time exceeded');
    }, this.options.maxHiddenTime);

    // Save current state
    this.autoSaveCurrentState();
    
    // Mask sensitive data
    this.maskSensitiveData();
  }

  handleTabVisible(data) {
    console.log('Tab visible - clearing hidden timer');
    // Clear hidden timer
    if (this.hiddenTimer) {
      clearTimeout(this.hiddenTimer);
      this.hiddenTimer = null;
    }

    // Unmask sensitive data
    this.unmaskSensitiveData();
    
    // Check if too much time has passed
    const timeHidden = data.timeInPreviousState;
    if (timeHidden > this.options.sensitiveDataTimeout) {
      this.requireReauthentication();
    } else {
      // Reset activity timer
      this.recordActivity();
    }
  }

  resetInactivityTimer() {
    // Clear existing timers
    if (this.inactivityTimer) clearTimeout(this.inactivityTimer);
    if (this.warningTimer) clearTimeout(this.warningTimer);
    
    this.hideWarning();

    // Set warning timer
    this.warningTimer = setTimeout(() => {
      this.showInactivityWarning();
    }, this.options.inactivityTimeout - this.options.warningTime);

    // Set logout timer
    this.inactivityTimer = setTimeout(() => {
      this.forceLogout('Session timeout due to inactivity');
    }, this.options.inactivityTimeout);
  }

  showInactivityWarning() {
    if (this.isWarningShown) return;
    
    this.isWarningShown = true;
    const remainingTime = Math.ceil(this.options.warningTime / 1000);
    
    this.createWarningModal(remainingTime);
  }

  createWarningModal(initialSeconds) {
    // Remove existing modal
    const existingModal = document.getElementById('inactivity-warning-modal');
    if (existingModal) {
      existingModal.remove();
    }

    // Create modal
    const modal = document.createElement('div');
    modal.id = 'inactivity-warning-modal';
    modal.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.7);
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: Arial, sans-serif;
    `;

    const modalContent = document.createElement('div');
    modalContent.style.cssText = `
      background: white;
      padding: 30px;
      border-radius: 8px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
      text-align: center;
      max-width: 400px;
      width: 90%;
    `;

    modalContent.innerHTML = `
      <h3 style="margin: 0 0 15px 0; color: #d32f2f;">Session Warning</h3>
      <p style="margin: 0 0 20px 0; color: #666;">
        Your session will expire in <span id="countdown">${initialSeconds}</span> seconds due to inactivity.
      </p>
      <div style="display: flex; gap: 10px; justify-content: center;">
        <button id="extend-session" style="
          background: #4CAF50;
          color: white;
          border: none;
          padding: 10px 20px;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
        ">Stay Logged In</button>
        <button id="logout-now" style="
          background: #f44336;
          color: white;
          border: none;
          padding: 10px 20px;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
        ">Logout Now</button>
      </div>
    `;

    modal.appendChild(modalContent);
    document.body.appendChild(modal);

    // Countdown timer
    let seconds = initialSeconds;
    const countdown = document.getElementById('countdown');
    const countdownInterval = setInterval(() => {
      seconds--;
      countdown.textContent = seconds;
      
      if (seconds <= 0) {
        clearInterval(countdownInterval);
        this.forceLogout('Session timeout');
      }
    }, 1000);

    // Event handlers
    document.getElementById('extend-session').onclick = () => {
      clearInterval(countdownInterval);
      this.extendSession();
    };

    document.getElementById('logout-now').onclick = () => {
      clearInterval(countdownInterval);
      this.forceLogout('User requested logout');
    };

    // Store countdown interval for cleanup
    modal.dataset.countdownInterval = countdownInterval;
  }

  hideWarning() {
    if (!this.isWarningShown) return;
    
    this.isWarningShown = false;
    const modal = document.getElementById('inactivity-warning-modal');
    if (modal) {
      // Clear countdown interval
      const countdownInterval = modal.dataset.countdownInterval;
      if (countdownInterval) {
        clearInterval(countdownInterval);
      }
      modal.remove();
    }
  }

  extendSession() {
    this.hideWarning();
    this.recordActivity();
    console.log('Session extended by user');
  }

  maskSensitiveData() {
    const sensitiveSelectors = [
      'input[type="password"]',
      'input[type="email"]',
      '.sensitive-data',
      '.account-number',
      '.credit-card',
      '.ssn'
    ];

    sensitiveSelectors.forEach(selector => {
      document.querySelectorAll(selector).forEach(element => {
        if (!element.dataset.originalValue) {
          element.dataset.originalValue = element.value || element.textContent;
        }
        
        if (element.tagName === 'INPUT') {
          element.value = '••••••••••';
          element.type = 'password';
        } else {
          element.textContent = '••••••••••';
        }
        
        element.classList.add('data-masked');
      });
    });

    console.log('Sensitive data masked');
  }

  unmaskSensitiveData() {
    document.querySelectorAll('.data-masked').forEach(element => {
      if (element.dataset.originalValue) {
        if (element.tagName === 'INPUT') {
          element.value = element.dataset.originalValue;
          // Restore original input type (if it wasn't password)
          if (element.dataset.originalType) {
            element.type = element.dataset.originalType;
          }
        } else {
          element.textContent = element.dataset.originalValue;
        }
        
        delete element.dataset.originalValue;
        element.classList.remove('data-masked');
      }
    });

    console.log('Sensitive data unmasked');
  }

  requireReauthentication() {
    console.log('Requiring reauthentication due to extended inactivity');
    
    // Create reauthentication modal
    this.createReauthModal();
  }

  createReauthModal() {
    const modal = document.createElement('div');
    modal.id = 'reauth-modal';
    modal.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      z-index: 1000000;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: Arial, sans-serif;
    `;

    modal.innerHTML = `
      <div style="
        background: white;
        padding: 30px;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        text-align: center;
        max-width: 400px;
        width: 90%;
      ">
        <h3 style="margin: 0 0 15px 0; color: #ff9800;">Reauthentication Required</h3>
        <p style="margin: 0 0 20px 0; color: #666;">
          For security reasons, please enter your password to continue.
        </p>
        <form id="reauth-form">
          <input type="password" id="reauth-password" placeholder="Enter your password" style="
            width: 100%;
            padding: 10px;
            margin: 0 0 15px 0;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
          ">
          <div style="display: flex; gap: 10px; justify-content: center;">
            <button type="submit" style="
              background: #4CAF50;
              color: white;
              border: none;
              padding: 10px 20px;
              border-radius: 4px;
              cursor: pointer;
              font-size: 14px;
            ">Authenticate</button>
            <button type="button" id="reauth-logout" style="
              background: #f44336;
              color: white;
              border: none;
              padding: 10px 20px;
              border-radius: 4px;
              cursor: pointer;
              font-size: 14px;
            ">Logout</button>
          </div>
        </form>
      </div>
    `;

    document.body.appendChild(modal);

    // Event handlers
    document.getElementById('reauth-form').onsubmit = (e) => {
      e.preventDefault();
      this.verifyReauthentication(document.getElementById('reauth-password').value);
    };

    document.getElementById('reauth-logout').onclick = () => {
      this.forceLogout('User chose to logout during reauthentication');
    };

    // Focus password field
    document.getElementById('reauth-password').focus();
  }

  async verifyReauthentication(password) {
    try {
      const response = await fetch('/api/auth/verify', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ password })
      });

      if (response.ok) {
        document.getElementById('reauth-modal').remove();
        this.recordActivity();
        console.log('Reauthentication successful');
      } else {
        this.showReauthError('Invalid password. Please try again.');
      }
    } catch (error) {
      this.showReauthError('Authentication failed. Please try again.');
    }
  }

  showReauthError(message) {
    const existingError = document.getElementById('reauth-error');
    if (existingError) {
      existingError.remove();
    }

    const error = document.createElement('div');
    error.id = 'reauth-error';
    error.style.cssText = `
      color: #f44336;
      font-size: 12px;
      margin: 10px 0;
    `;
    error.textContent = message;

    const form = document.getElementById('reauth-form');
    form.insertBefore(error, form.firstChild);

    // Clear password field
    document.getElementById('reauth-password').value = '';
    document.getElementById('reauth-password').focus();
  }

  startAutoSave() {
    this.autoSaveTimer = setInterval(() => {
      this.autoSaveCurrentState();
    }, this.options.autoSaveInterval);
  }

  autoSaveCurrentState() {
    // Collect form data
    const formData = this.collectFormData();
    
    if (Object.keys(formData).length > 0) {
      localStorage.setItem('autoSave_' + Date.now(), JSON.stringify({
        data: formData,
        timestamp: Date.now(),
        url: window.location.href
      }));

      // Keep only last 5 auto-saves
      this.cleanupAutoSaves();
      
      console.log('Auto-saved form data');
    }
  }

  collectFormData() {
    const formData = {};
    const forms = document.querySelectorAll('form');
    
    forms.forEach((form, formIndex) => {
      const inputs = form.querySelectorAll('input, textarea, select');
      inputs.forEach((input, inputIndex) => {
        if (input.type !== 'password' && input.value.trim()) {
          const key = input.name || input.id || `form${formIndex}_input${inputIndex}`;
          formData[key] = input.value;
        }
      });
    });

    return formData;
  }

  cleanupAutoSaves() {
    const keys = Object.keys(localStorage)
      .filter(key => key.startsWith('autoSave_'))
      .sort()
      .reverse();

    // Remove old auto-saves (keep only 5)
    keys.slice(5).forEach(key => {
      localStorage.removeItem(key);
    });
  }

  forceLogout(reason) {
    console.log('Force logout:', reason);
    
    // Clear all timers
    [this.inactivityTimer, this.warningTimer, this.hiddenTimer, this.autoSaveTimer]
      .forEach(timer => timer && clearTimeout(timer));

    // Save final state
    this.autoSaveCurrentState();
    
    // Clear sensitive data
    this.clearSensitiveData();
    
    // Redirect to logout
    if (this.options.logoutCallback) {
      this.options.logoutCallback(reason);
    } else {
      window.location.href = '/logout?reason=' + encodeURIComponent(reason);
    }
  }

  clearSensitiveData() {
    // Clear form data
    document.querySelectorAll('input[type="password"]').forEach(input => {
      input.value = '';
    });

    // Clear any application-specific sensitive data
    if (this.options.clearDataCallback) {
      this.options.clearDataCallback();
    }
  }

  // Utility function for throttling
  throttle(func, wait) {
    let timeout;
    let previous = 0;
    
    return function(...args) {
      const now = Date.now();
      if (now - previous > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        func.apply(this, args);
      } else if (!timeout) {
        timeout = setTimeout(() => {
          previous = Date.now();
          timeout = null;
          func.apply(this, args);
        }, wait - (now - previous));
      }
    };
  }

  // Public methods
  getSessionInfo() {
    return {
      lastActivity: this.lastActivity,
      timeSinceLastActivity: Date.now() - this.lastActivity,
      isWarningShown: this.isWarningShown,
      inactivityTimeout: this.options.inactivityTimeout
    };
  }

  extendSessionManually(additionalTime = 900000) { // 15 minutes default
    this.lastActivity = Date.now() + additionalTime;
    this.resetInactivityTimer();
    console.log('Session extended manually by', additionalTime / 1000, 'seconds');
  }
}

// Usage example
const securityManager = new SecurityVisibilityManager({
  inactivityTimeout: 1800000, // 30 minutes
  warningTime: 300000, // 5 minutes warning
  sensitiveDataTimeout: 600000, // 10 minutes for tab hidden
  maxHiddenTime: 3600000, // 1 hour maximum hidden
  logoutCallback: (reason) => {
    console.log('Logout triggered:', reason);
    // Custom logout logic here
    fetch('/api/auth/logout', {
      method: 'POST',
      body: JSON.stringify({ reason })
    }).then(() => {
      window.location.href = '/login';
    });
  },
  clearDataCallback: () => {
    // Clear application-specific data
    sessionStorage.clear();
    // Clear any application state
  }
});

Preventing Sensitive Data Exposure When Tab Is Hidden

Advanced Data Protection System:

class SensitiveDataProtector {
  constructor() {
    this.tabManager = new TabVisibilityManager();
    this.protectionLevels = {
      'low': { maskDelay: 300000, blurDelay: 0 }, // 5 minutes
      'medium': { maskDelay: 60000, blurDelay: 0 }, // 1 minute
      'high': { maskDelay: 5000, blurDelay: 0 }, // 5 seconds
      'critical': { maskDelay: 0, blurDelay: 0 } // Immediate
    };
    
    this.protectedElements = new Map();
    this.blurOverlay = null;
    this.maskingTimer = null;
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden(() => this.activateProtection())
      .onVisible(() => this.deactivateProtection());

    // Register sensitive elements automatically
    this.autoRegisterSensitiveElements();
    
    // Watch for new elements
    this.setupElementObserver();
  }

  autoRegisterSensitiveElements() {
    const sensitiveSelectors = {
      'critical': [
        'input[type="password"]',
        '.ssn',
        '.credit-card-number',
        '.bank-account'
      ],
      'high': [
        'input[type="email"]',
        '.phone-number',
        '.address',
        '.financial-data'
      ],
      'medium': [
        '.personal-info',
        '.user-profile',
        '.account-details'
      ],
      'low': [
        '.general-content',
        '.non-critical'
      ]
    };

    Object.entries(sensitiveSelectors).forEach(([level, selectors]) => {
      selectors.forEach(selector => {
        document.querySelectorAll(selector).forEach(element => {
          this.registerElement(element, level);
        });
      });
    });
  }

  registerElement(element, protectionLevel = 'medium') {
    if (!this.protectedElements.has(element)) {
      this.protectedElements.set(element, {
        level: protectionLevel,
        originalValue: element.value || element.textContent,
        originalType: element.type,
        isProtected: false
      });
    }
  }

  activateProtection() {
    // Apply immediate protection for critical elements
    this.protectElementsByLevel('critical');
    
    // Set timer for other levels
    this.maskingTimer = setTimeout(() => {
      this.protectElementsByLevel('high');
      
      setTimeout(() => {
        this.protectElementsByLevel('medium');
        
        setTimeout(() => {
          this.protectElementsByLevel('low');
        }, 60000); // 1 minute more for low
      }, 60000); // 1 minute more for medium
    }, 5000); // 5 seconds for high

    // Apply blur overlay for visual protection
    this.createBlurOverlay();
  }

  protectElementsByLevel(level) {
    this.protectedElements.forEach((data, element) => {
      if (data.level === level && !data.isProtected) {
        this.protectElement(element, data);
      }
    });
  }

  protectElement(element, data) {
    if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
      element.dataset.originalValue = element.value;
      element.dataset.originalType = element.type;
      element.value = '••••••••••';
      element.type = 'password';
      element.readOnly = true;
    } else {
      element.dataset.originalText = element.textContent;
      element.textContent = '••••••••••';
    }

    element.classList.add('data-protected');
    data.isProtected = true;
    
    console.log(`Protected ${data.level} level element`);
  }

  createBlurOverlay() {
    if (this.blurOverlay) return;

    this.blurOverlay = document.createElement('div');
    this.blurOverlay.id = 'security-blur-overlay';
    this.blurOverlay.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.1);
      backdrop-filter: blur(5px);
      z-index: 999998;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.3s ease-in-out;
    `;

    document.body.appendChild(this.blurOverlay);

    // Fade in
    setTimeout(() => {
      this.blurOverlay.style.opacity = '1';
    }, 100);
  }

  deactivateProtection() {
    // Clear protection timer
    if (this.maskingTimer) {
      clearTimeout(this.maskingTimer);
      this.maskingTimer = null;
    }

    // Remove blur overlay
    this.removeBlurOverlay();
    
    // Restore all protected elements
    this.protectedElements.forEach((data, element) => {
      if (data.isProtected) {
        this.restoreElement(element, data);
      }
    });
  }

  restoreElement(element, data) {
    if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
      element.value = element.dataset.originalValue || data.originalValue;
      element.type = element.dataset.originalType || data.originalType;
      element.readOnly = false;
    } else {
      element.textContent = element.dataset.originalText || data.originalValue;
    }

    element.classList.remove('data-protected');
    data.isProtected = false;
    
    // Clean up data attributes
    delete element.dataset.originalValue;
    delete element.dataset.originalType;
    delete element.dataset.originalText;
  }

  removeBlurOverlay() {
    if (this.blurOverlay) {
      this.blurOverlay.style.opacity = '0';
      setTimeout(() => {
        if (this.blurOverlay && this.blurOverlay.parentElement) {
          this.blurOverlay.remove();
        }
        this.blurOverlay = null;
      }, 300);
    }
  }

  setupElementObserver() {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            // Check if the added node itself is sensitive
            if (this.isSensitiveElement(node)) {
              const level = this.determineSensitivityLevel(node);
              this.registerElement(node, level);
            }

            // Check for sensitive child elements
            const sensitiveChildren = node.querySelectorAll ? 
              node.querySelectorAll(this.getSensitiveSelectors().join(', ')) : [];
            
            sensitiveChildren.forEach(child => {
              const level = this.determineSensitivityLevel(child);
              this.registerElement(child, level);
            });
          }
        });
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  isSensitiveElement(element) {
    const sensitiveSelectors = this.getSensitiveSelectors();
    return sensitiveSelectors.some(selector => {
      try {
        return element.matches(selector);
      } catch (e) {
        return false;
      }
    });
  }

  getSensitiveSelectors() {
    return [
      'input[type="password"]',
      'input[type="email"]',
      '.ssn', '.credit-card-number', '.bank-account',
      '.phone-number', '.address', '.financial-data',
      '.personal-info', '.user-profile', '.account-details'
    ];
  }

  determineSensitivityLevel(element) {
    const criticalSelectors = ['input[type="password"]', '.ssn', '.credit-card-number', '.bank-account'];
    const highSelectors = ['input[type="email"]', '.phone-number', '.address', '.financial-data'];
    const mediumSelectors = ['.personal-info', '.user-profile', '.account-details'];
    
    if (criticalSelectors.some(selector => element.matches(selector))) {
      return 'critical';
    } else if (highSelectors.some(selector => element.matches(selector))) {
      return 'high';
    } else if (mediumSelectors.some(selector => element.matches(selector))) {
      return 'medium';
    }
    return 'low';
  }

  // Public methods
  setProtectionLevel(element, level) {
    if (this.protectedElements.has(element)) {
      this.protectedElements.get(element).level = level;
    } else {
      this.registerElement(element, level);
    }
  }

  forceProtectAll() {
    this.protectedElements.forEach((data, element) => {
      if (!data.isProtected) {
        this.protectElement(element, data);
      }
    });
    this.createBlurOverlay();
  }

  forceRestoreAll() {
    this.protectedElements.forEach((data, element) => {
      if (data.isProtected) {
        this.restoreElement(element, data);
      }
    });
    this.removeBlurOverlay();
  }
}

// Usage
const dataProtector = new SensitiveDataProtector();

// Register custom sensitive elements
document.querySelectorAll('.custom-sensitive').forEach(element => {
  dataProtector.registerElement(element, 'high');
});

Combining Visibility Events with Session Timers

Integrated Session and Visibility Management:

class IntegratedSessionManager {
  constructor(options = {}) {
    this.options = {
      sessionTimeout: options.sessionTimeout || 1800000, // 30 minutes
      tabHiddenMultiplier: options.tabHiddenMultiplier || 0.5, // Count hidden time as half
      warningThreshold: options.warningThreshold || 0.9, // Warn at 90% of timeout
      extendOnActivity: options.extendOnActivity !== false,
      saveStateInterval: options.saveStateInterval || 60000, // 1 minute
      ...options
    };

    this.tabManager = new TabVisibilityManager();
    this.securityManager = new SecurityVisibilityManager(this.options);
    
    this.sessionState = {
      startTime: Date.now(),
      effectiveTime: 0, // Time counting towards session limit
      hiddenTime: 0,
      visibleTime: 0,
      lastActivity: Date.now(),
      isActive: true
    };

    this.sessionTimer = null;
    this.stateUpdateInterval = null;
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden((data) => this.handleTabHidden(data))
      .onVisible((data) => this.handleTabVisible(data));

    this.startSessionTracking();
    this.startStateUpdates();
    
    // Track user activity
    this.setupActivityTracking();
  }

  handleTabHidden(data) {
    console.log('Tab hidden - adjusting session timer');
    this.updateEffectiveTime();
    
    // Slower session countdown when hidden
    this.adjustSessionTimer(this.options.tabHiddenMultiplier);
  }

  handleTabVisible(data) {
    console.log('Tab visible - restoring normal session timer');
    this.updateEffectiveTime();
    
    // Normal session countdown when visible
    this.adjustSessionTimer(1.0);
    
    // Check if session should be extended due to return
    if (data.timeInPreviousState > 60000) { // If away for more than 1 minute
      this.handleUserReturn(data.timeInPreviousState);
    }
  }

  updateEffectiveTime() {
    const now = Date.now();
    const timeSinceLastUpdate = now - this.sessionState.lastActivity;
    
    if (document.hidden) {
      // Count hidden time with multiplier
      this.sessionState.effectiveTime += timeSinceLastUpdate * this.options.tabHiddenMultiplier;
      this.sessionState.hiddenTime += timeSinceLastUpdate;
    } else {
      // Count visible time normally
      this.sessionState.effectiveTime += timeSinceLastUpdate;
      this.sessionState.visibleTime += timeSinceLastUpdate;
    }
    
    this.sessionState.lastActivity = now;
  }

  adjustSessionTimer(multiplier) {
    if (this.sessionTimer) {
      clearTimeout(this.sessionTimer);
    }

    const remainingTime = this.getRemainingSessionTime();
    const adjustedTime = remainingTime / multiplier;

    this.sessionTimer = setTimeout(() => {
      this.handleSessionExpiry();
    }, adjustedTime);

    console.log(`Session timer adjusted: ${adjustedTime}ms remaining (multiplier: ${multiplier})`);
  }

  startSessionTracking() {
    this.sessionTimer = setTimeout(() => {
      this.handleSessionExpiry();
    }, this.options.sessionTimeout);
  }

  startStateUpdates() {
    this.stateUpdateInterval = setInterval(() => {
      this.updateSessionState();
      this.checkWarningThreshold();
      this.saveSessionState();
    }, this.options.saveStateInterval);
  }

  updateSessionState() {
    this.updateEffectiveTime();
    
    const sessionProgress = this.getSessionProgress();
    
    // Emit session update event
    document.dispatchEvent(new CustomEvent('sessionUpdate', {
      detail: {
        progress: sessionProgress,
        remainingTime: this.getRemainingSessionTime(),
        effectiveTime: this.sessionState.effectiveTime,
        isHidden: document.hidden
      }
    }));
  }

  checkWarningThreshold() {
    const progress = this.getSessionProgress();
    
    if (progress >= this.options.warningThreshold && !this.warningShown) {
      this.showSessionWarning();
      this.warningShown = true;
    }
  }

  showSessionWarning() {
    const remainingTime = this.getRemainingSessionTime();
    const minutes = Math.ceil(remainingTime / 60000);
    
    console.log(`Session warning: ${minutes} minutes remaining`);
    
    // Create session warning notification
    this.createSessionWarningBanner(minutes);
  }

  createSessionWarningBanner(minutes) {
    const banner = document.createElement('div');
    banner.id = 'session-warning-banner';
    banner.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      background: linear-gradient(90deg, #ff9800, #f57c00);
      color: white;
      padding: 10px 20px;
      text-align: center;
      z-index: 999999;
      font-family: Arial, sans-serif;
      font-size: 14px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    `;

    banner.innerHTML = `
      <span>⚠️ Session expires in ${minutes} minute${minutes > 1 ? 's' : ''}. Activity will extend your session.</span>
      <button onclick="this.parentElement.remove(); this.onclick=null;" style="
        background: rgba(255,255,255,0.2);
        border: none;
        color: white;
        padding: 5px 10px;
        margin-left: 15px;
        border-radius: 3px;
        cursor: pointer;
        font-size: 12px;
      ">Dismiss</button>
    `;

    document.body.appendChild(banner);

    // Auto-remove after 10 seconds if not dismissed
    setTimeout(() => {
      if (banner.parentElement) {
        banner.remove();
      }
    }, 10000);
  }

  handleUserReturn(timeAway) {
    // Extend session if user returns after brief absence
    if (timeAway > 60000 && timeAway < 600000) { // 1-10 minutes
      const extensionTime = Math.min(timeAway * 0.5, 300000); // Max 5 minutes extension
      this.extendSession(extensionTime);
      
      console.log(`Session extended by ${extensionTime / 1000}s due to user return`);
    }
  }

  extendSession(additionalTime = 900000) { // 15 minutes default
    this.sessionState.effectiveTime -= additionalTime;
    this.warningShown = false;
    
    // Remove any existing warning
    const warning = document.getElementById('session-warning-banner');
    if (warning) {
      warning.remove();
    }
    
    // Restart timer with new time
    this.adjustSessionTimer(document.hidden ? this.options.tabHiddenMultiplier : 1.0);
    
    console.log(`Session extended by ${additionalTime / 1000} seconds`);
  }

  setupActivityTracking() {
    const activityEvents = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart'];
    
    activityEvents.forEach(event => {
      document.addEventListener(event, this.handleActivity.bind(this), { passive: true });
    });

    // Throttle activity handling
    this.handleActivity = this.throttle(this.handleActivity, 5000); // Max once per 5 seconds
  }

  handleActivity() {
    if (this.options.extendOnActivity) {
      const timeSinceLastActivity = Date.now() - this.sessionState.lastActivity;
      
      // Extend session on significant activity
      if (timeSinceLastActivity > 300000) { // 5 minutes of inactivity
        this.extendSession(300000); // Extend by 5 minutes
      }
    }
    
    this.sessionState.lastActivity = Date.now();
  }

  handleSessionExpiry() {
    console.log('Session expired');
    
    this.saveSessionState();
    
    // Create session expired modal
    this.createSessionExpiredModal();
  }

  createSessionExpiredModal() {
    const modal = document.createElement('div');
    modal.id = 'session-expired-modal';
    modal.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      z-index: 1000001;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: Arial, sans-serif;
    `;

    modal.innerHTML = `
      <div style="
        background: white;
        padding: 40px;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        text-align: center;
        max-width: 450px;
        width: 90%;
      ">
        <h2 style="margin: 0 0 20px 0; color: #d32f2f;">Session Expired</h2>
        <p style="margin: 0 0 20px 0; color: #666; line-height: 1.5;">
          Your session has expired for security reasons. Please log in again to continue.
        </p>
        <div style="margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 4px; font-size: 12px; color: #777;">
          <strong>Session Summary:</strong><br>
          Total time: ${this.formatTime(Date.now() - this.sessionState.startTime)}<br>
          Active time: ${this.formatTime(this.sessionState.visibleTime)}<br>
          Hidden time: ${this.formatTime(this.sessionState.hiddenTime)}
        </div>
        <div style="display: flex; gap: 10px; justify-content: center;">
          <button onclick="window.location.href='/login'" style="
            background: #4CAF50;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
          ">Login Again</button>
          <button onclick="window.location.href='/'" style="
            background: #757575;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
          ">Go Home</button>
        </div>
      </div>
    `;

    document.body.appendChild(modal);
    
    // Prevent interaction with page
    document.body.style.overflow = 'hidden';
  }

  saveSessionState() {
    const stateData = {
      ...this.sessionState,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    };

    try {
      localStorage.setItem('sessionState', JSON.stringify(stateData));
      
      // Also send to server for analytics
      this.sendSessionAnalytics(stateData);
    } catch (error) {
      console.error('Failed to save session state:', error);
    }
  }

  async sendSessionAnalytics(stateData) {
    try {
      await fetch('/api/analytics/session', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(stateData),
        keepalive: true // Ensure delivery even during page unload
      });
    } catch (error) {
      console.error('Failed to send session analytics:', error);
    }
  }

  // Utility methods
  getRemainingSessionTime() {
    return Math.max(0, this.options.sessionTimeout - this.sessionState.effectiveTime);
  }

  getSessionProgress() {
    return Math.min(1, this.sessionState.effectiveTime / this.options.sessionTimeout);
  }

  formatTime(milliseconds) {
    const seconds = Math.floor(milliseconds / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    
    if (hours > 0) {
      return `${hours}h ${minutes % 60}m`;
    } else if (minutes > 0) {
      return `${minutes}m ${seconds % 60}s`;
    } else {
      return `${seconds}s`;
    }
  }

  throttle(func, wait) {
    let timeout;
    let previous = 0;
    
    return function(...args) {
      const now = Date.now();
      if (now - previous > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        func.apply(this, args);
      } else if (!timeout) {
        timeout = setTimeout(() => {
          previous = Date.now();
          timeout = null;
          func.apply(this, args);
        }, wait - (now - previous));
      }
    };
  }

  // Public API
  getSessionInfo() {
    return {
      ...this.sessionState,
      remainingTime: this.getRemainingSessionTime(),
      progress: this.getSessionProgress(),
      isHidden: document.hidden
    };
  }

  forceExtendSession(time = 900000) {
    this.extendSession(time);
  }

  forceExpireSession() {
    this.handleSessionExpiry();
  }
}

// Initialize integrated session management
const sessionManager = new IntegratedSessionManager({
  sessionTimeout: 1800000, // 30 minutes
  tabHiddenMultiplier: 0.3, // Hidden time counts as 30% of normal time
  warningThreshold: 0.85, // Warn at 85% of session time
  extendOnActivity: true,
  saveStateInterval: 30000 // Save state every 30 seconds
});

// Listen for session updates
document.addEventListener('sessionUpdate', (event) => {
  const { progress, remainingTime, isHidden } = event.detail;
  
  // Update UI elements showing session status
  const sessionIndicator = document.getElementById('session-indicator');
  if (sessionIndicator) {
    sessionIndicator.textContent = `Session: ${Math.ceil(remainingTime / 60000)}min remaining`;
    sessionIndicator.style.opacity = isHidden ? '0.5' : '1';
  }
});

// Manual session extension button
function extendCurrentSession() {
  sessionManager.forceExtendSession(900000); // 15 minutes
  alert('Session extended by 15 minutes');
}

Optimizing Performance and User Experience

Reducing Resource Consumption During Inactivity

Implement intelligent resource management based on tab visibility:

class ResourceOptimizationManager {
  constructor() {
    this.tabManager = new TabVisibilityManager();
    this.optimizations = {
      intervals: new Map(),
      animations: new Set(),
      connections: new Set(),
      pollingTasks: new Map(),
      mediaElements: new Set()
    };
    
    this.performanceMode = 'normal'; // 'normal', 'power-saver', 'aggressive'
    this.inactivityTimer = null;
    this.batteryInfo = null;
    
    this.init();
  }

  async init() {
    this.tabManager
      .onHidden(() => this.activateOptimizations())
      .onVisible(() => this.deactivateOptimizations());

    // Get battery information if available
    if ('getBattery' in navigator) {
      try {
        this.batteryInfo = await navigator.getBattery();
        this.setupBatteryOptimizations();
      } catch (error) {
        console.log('Battery API not available');
      }
    }

    // Auto-detect resource-intensive operations
    this.detectResourceIntensiveOperations();
    
    // Monitor performance
    this.startPerformanceMonitoring();
  }

  setupBatteryOptimizations() {
    if (!this.batteryInfo) return;

    const updateOptimizationLevel = () => {
      if (this.batteryInfo.level < 0.2 && !this.batteryInfo.charging) {
        this.performanceMode = 'aggressive';
      } else if (this.batteryInfo.level < 0.5 && !this.batteryInfo.charging) {
        this.performanceMode = 'power-saver';
      } else {
        this.performanceMode = 'normal';
      }
      
      console.log(`Performance mode: ${this.performanceMode} (battery: ${Math.round(this.batteryInfo.level * 100)}%)`);
    };

    this.batteryInfo.addEventListener('chargingchange', updateOptimizationLevel);
    this.batteryInfo.addEventListener('levelchange', updateOptimizationLevel);
    
    updateOptimizationLevel();
  }

  activateOptimizations() {
    console.log('Activating resource optimizations');
    
    this.pauseIntervals();
    this.pauseAnimations();
    this.reduceNetworkActivity();
    this.pauseMediaPlayback();
    this.scheduleGarbageCollection();
    
    // Set inactivity timer for deeper optimizations
    this.inactivityTimer = setTimeout(() => {
      this.activateDeepOptimizations();
    }, 60000); // After 1 minute of inactivity
  }

  deactivateOptimizations() {
    console.log('Deactivating resource optimizations');
    
    if (this.inactivityTimer) {
      clearTimeout(this.inactivityTimer);
      this.inactivityTimer = null;
    }
    
    this.resumeIntervals();
    this.resumeAnimations();
    this.restoreNetworkActivity();
    this.resumeMediaPlayback();
    this.deactivateDeepOptimizations();
  }

  pauseIntervals() {
    // Store and pause all registered intervals
    this.optimizations.intervals.forEach((data, intervalId) => {
      clearInterval(intervalId);
      data.paused = true;
    });
    
    // Find and pause common polling intervals
    this.pausePollingOperations();
  }

  resumeIntervals() {
    this.optimizations.intervals.forEach((data, intervalId) => {
      if (data.paused) {
        const newId = setInterval(data.callback, data.interval);
        this.optimizations.intervals.delete(intervalId);
        this.optimizations.intervals.set(newId, { ...data, paused: false });
      }
    });
  }

  pauseAnimations() {
    // Cancel all tracked animation frames
    this.optimizations.animations.forEach(animationId => {
      cancelAnimationFrame(animationId);
    });
    
    // Pause CSS animations
    document.documentElement.style.setProperty('--animation-play-state', 'paused');
    
    // Dispatch event for custom animation handling
    document.dispatchEvent(new CustomEvent('pauseAnimations'));
  }

  resumeAnimations() {
    // Resume CSS animations
    document.documentElement.style.setProperty('--animation-play-state', 'running');
    
    // Dispatch event for custom animation handling
    document.dispatchEvent(new CustomEvent('resumeAnimations'));
  }

  reduceNetworkActivity() {
    // Reduce polling frequency for non-critical requests
    this.optimizations.pollingTasks.forEach((task, taskId) => {
      if (task.reducible) {
        clearInterval(task.intervalId);
        
        const reducedInterval = task.originalInterval * (
          this.performanceMode === 'aggressive' ? 10 : 
          this.performanceMode === 'power-saver' ? 5 : 2
        );
        
        task.intervalId = setInterval(task.callback, reducedInterval);
        console.log(`Reduced polling for ${taskId} from ${task.originalInterval}ms to ${reducedInterval}ms`);
      }
    });
    
    // Pause WebSocket heartbeats if not critical
    this.pauseWebSocketHeartbeats();
  }

  restoreNetworkActivity() {
    // Restore original polling frequencies
    this.optimizations.pollingTasks.forEach((task, taskId) => {
      if (task.reducible && task.intervalId) {
        clearInterval(task.intervalId);
        task.intervalId = setInterval(task.callback, task.originalInterval);
        console.log(`Restored polling for ${taskId} to ${task.originalInterval}ms`);
      }
    });
    
    // Resume WebSocket heartbeats
    this.resumeWebSocketHeartbeats();
  }

  pauseMediaPlayback() {
    this.optimizations.mediaElements.forEach(element => {
      if (!element.paused && !element.ended) {
        element.dataset.wasPlayingBeforeOptimization = 'true';
        element.pause();
      }
    });
  }

  resumeMediaPlayback() {
    this.optimizations.mediaElements.forEach(element => {
      if (element.dataset.wasPlayingBeforeOptimization === 'true') {
        element.play().catch(error => {
          console.log('Could not resume media playback:', error);
        });
        delete element.dataset.wasPlayingBeforeOptimization;
      }
    });
  }

  activateDeepOptimizations() {
    console.log('Activating deep optimizations after extended inactivity');
    
    // Reduce memory usage
    this.performMemoryCleanup();
    
    // Suspend heavy operations
    this.suspendHeavyOperations();
    
    // Reduce visual updates
    this.reduceVisualUpdates();
  }

  deactivateDeepOptimizations() {
    console.log('Deactivating deep optimizations');
    
    // Resume heavy operations
    this.resumeHeavyOperations();
    
    // Restore visual updates
    this.restoreVisualUpdates();
  }

  performMemoryCleanup() {
    // Clear caches
    if ('caches' in window) {
      caches.keys().then(names => {
        names.forEach(name => {
          if (name.includes('temp') || name.includes('cache')) {
            caches.delete(name);
          }
        });
      });
    }
    
    // Clear large data structures if possible
    document.dispatchEvent(new CustomEvent('performMemoryCleanup'));
    
    // Force garbage collection if available (Chrome DevTools)
    if (window.gc && typeof window.gc === 'function') {
      window.gc();
    }
  }

  scheduleGarbageCollection() {
    // Schedule periodic cleanup during inactivity
    setTimeout(() => {
      if (document.hidden) {
        this.performMemoryCleanup();
      }
    }, 30000);
  }

  // Registration methods for applications to use
  registerInterval(intervalId, callback, interval, options = {}) {
    this.optimizations.intervals.set(intervalId, {
      callback,
      interval,
      paused: false,
      critical: options.critical || false,
      reducible: options.reducible !== false
    });
  }

  registerAnimation(animationId) {
    this.optimizations.animations.add(animationId);
  }

  registerPollingTask(taskId, callback, interval, options = {}) {
    this.optimizations.pollingTasks.set(taskId, {
      callback,
      originalInterval: interval,
      intervalId: setInterval(callback, interval),
      reducible: options.reducible !== false,
      critical: options.critical || false
    });
  }

  registerMediaElement(element) {
    this.optimizations.mediaElements.add(element);
  }

  // Performance monitoring
  startPerformanceMonitoring() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach(entry => {
          if (entry.entryType === 'measure' && entry.duration > 16.67) { // Slower than 60fps
            console.warn(`Slow operation detected: ${entry.name} took ${entry.duration}ms`);
          }
        });
      });
      
      observer.observe({ entryTypes: ['measure', 'navigation'] });
    }
    
    // Monitor memory usage
    setInterval(() => {
      if ('memory' in performance) {
        const memory = performance.memory;
        const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
        
        if (usagePercent > 80) {
          console.warn(`High memory usage: ${usagePercent.toFixed(1)}%`);
          if (document.hidden) {
            this.performMemoryCleanup();
          }
        }
      }
    }, 30000);
  }

  detectResourceIntensiveOperations() {
    // Override common resource-intensive methods to track them
    const originalSetInterval = window.setInterval;
    const originalRequestAnimationFrame = window.requestAnimationFrame;
    
    window.setInterval = (callback, delay, ...args) => {
      const id = originalSetInterval.call(window, callback, delay, ...args);
      
      // Auto-register intervals that run frequently
      if (delay < 1000) {
        this.registerInterval(id, callback, delay, { reducible: true });
      }
      
      return id;
    };
    
    window.requestAnimationFrame = (callback) => {
      const id = originalRequestAnimationFrame.call(window, callback);
      this.registerAnimation(id);
      return id;
    };
  }

  // Utility methods for specific optimizations
  pausePollingOperations() {
    // Look for common polling patterns
    const potentialPolling = Array.from(this.optimizations.intervals.entries())
      .filter(([id, data]) => data.interval < 10000 && data.reducible);
    
    console.log(`Found ${potentialPolling.length} potential polling operations to optimize`);
  }

  pauseWebSocketHeartbeats() {
    // Dispatch event for WebSocket connections to reduce heartbeat frequency
    document.dispatchEvent(new CustomEvent('reduceWebSocketActivity'));
  }

  resumeWebSocketHeartbeats() {
    document.dispatchEvent(new CustomEvent('restoreWebSocketActivity'));
  }

  suspendHeavyOperations() {
    document.dispatchEvent(new CustomEvent('suspendHeavyOperations'));
  }

  resumeHeavyOperations() {
    document.dispatchEvent(new CustomEvent('resumeHeavyOperations'));
  }

  reduceVisualUpdates() {
    // Reduce update frequency for charts, graphs, etc.
    document.dispatchEvent(new CustomEvent('reduceVisualUpdates'));
  }

  restoreVisualUpdates() {
    document.dispatchEvent(new CustomEvent('restoreVisualUpdates'));
  }

  // Public API
  setPerformanceMode(mode) {
    if (['normal', 'power-saver', 'aggressive'].includes(mode)) {
      this.performanceMode = mode;
      console.log(`Performance mode set to: ${mode}`);
    }
  }

  getOptimizationStatus() {
    return {
      performanceMode: this.performanceMode,
      isOptimized: document.hidden,
      intervals: this.optimizations.intervals.size,
      animations: this.optimizations.animations.size,
      pollingTasks: this.optimizations.pollingTasks.size,
      mediaElements: this.optimizations.mediaElements.size,
      batteryLevel: this.batteryInfo ? this.batteryInfo.level : null
    };
  }
}

// Initialize resource optimization
const resourceManager = new ResourceOptimizationManager();

// Example usage - register operations for optimization
resourceManager.registerPollingTask('api_status', () => {
  // Check API status
  fetch('/api/status').then(response => {
    console.log('API status check');
  });
}, 5000, { reducible: true });

// Register critical polling that shouldn't be reduced
resourceManager.registerPollingTask('critical_alerts', () => {
  // Check for critical alerts
  fetch('/api/alerts/critical').then(response => {
    console.log('Critical alerts check');
  });
}, 10000, { critical: true, reducible: false });

// Listen for optimization events
document.addEventListener('performMemoryCleanup', () => {
  // Application-specific memory cleanup
  window.applicationCache = {};
  console.log('Application memory cleaned up');
});

document.addEventListener('suspendHeavyOperations', () => {
  // Stop heavy computational tasks
  console.log('Suspending heavy operations');
});

Best Practices for Smooth User Transitions

Create seamless experiences when users switch between tabs:

class SmoothTransitionManager {
  constructor() {
    this.tabManager = new TabVisibilityManager();
    this.transitionState = {
      isTransitioning: false,
      lastVisibilityChange: Date.now(),
      transitionQueue: []
    };
    
    this.animations = new Map();
    this.stateSnapshots = new Map();
    
    this.init();
  }

  init() {
    this.tabManager
      .onHidden((data) => this.handleHiddenTransition(data))
      .onVisible((data) => this.handleVisibleTransition(data));
  }

  handleHiddenTransition(data) {
    if (this.transitionState.isTransitioning) return;
    
    this.transitionState.isTransitioning = true;
    console.log('Starting hidden transition');
    
    // Save current state
    this.saveCurrentState();
    
    // Queue transition animations
    this.queueTransition('fadeOut', () => this.fadeOutNonEssentials());
    this.queueTransition('pauseAnimations', () => this.pauseAllAnimations());
    this.queueTransition('saveProgress', () => this.saveUserProgress());
    
    this.executeTransitionQueue().then(() => {
      this.transitionState.isTransitioning = false;
    });
  }

  handleVisibleTransition(data) {
    if (this.transitionState.isTransitioning) return;
    
    this.transitionState.isTransitioning = true;
    console.log('Starting visible transition');
    
    // Calculate time away
    const timeAway = data.timeInPreviousState;
    
    // Queue transition animations based on time away
    if (timeAway > 30000) { // More than 30 seconds
      this.queueTransition('welcome', () => this.showWelcomeBackAnimation());
    }
    
    this.queueTransition('restoreState', () => this.restoreState());
    this.queueTransition('fadeIn', () => this.fadeInElements());
    this.queueTransition('resumeAnimations', () => this.resumeAllAnimations());
    this.queueTransition('refreshData', () => this.refreshStaleData(timeAway));
    
    this.executeTransitionQueue().then(() => {
      this.transitionState.isTransitioning = false;
    });
  }

  queueTransition(name, action, delay = 0) {
    this.transitionState.transitionQueue.push({
      name,
      action,
      delay,
      timestamp: Date.now()
    });
  }

  async executeTransitionQueue() {
    console.log(`Executing ${this.transitionState.transitionQueue.length} transitions`);
    
    for (const transition of this.transitionState.transitionQueue) {
      try {
        if (transition.delay > 0) {
          await this.delay(transition.delay);
        }
        
        await transition.action();
        console.log(`Completed transition: ${transition.name}`);
      } catch (error) {
        console.error(`Failed transition: ${transition.name}`, error);
      }
    }
    
    this.transitionState.transitionQueue = [];
  }

  saveCurrentState() {
    const state = {
      scrollPosition: {
        x: window.pageXOffset,
        y: window.pageYOffset
      },
      formData: this.collectFormData(),
      activeElement: document.activeElement ? {
        tagName: document.activeElement.tagName,
        id: document.activeElement.id,
        className: document.activeElement.className,
        selectionStart: document.activeElement.selectionStart,
        selectionEnd: document.activeElement.selectionEnd
      } : null,
      timestamp: Date.now()
    };
    
    this.stateSnapshots.set('beforeHidden', state);
    console.log('Current state saved');
  }

  restoreState() {
    const state = this.stateSnapshots.get('beforeHidden');
    if (!state) return;
    
    // Restore scroll position smoothly
    if (state.scrollPosition) {
      window.scrollTo({
        left: state.scrollPosition.x,
        top: state.scrollPosition.y,
        behavior: 'smooth'
      });
    }
    
    // Restore form data
    this.restoreFormData(state.formData);
    
    // Restore focus
    if (state.activeElement) {
      this.restoreFocus(state.activeElement);
    }
    
    console.log('State restored');
  }

  collectFormData() {
    const formData = {};
    
    document.querySelectorAll('input, textarea, select').forEach((element, index) => {
      if (element.type !== 'password' && element.value) {
        const key = element.name || element.id || `element_${index}`;
        formData[key] = {
          value: element.value,
          type: element.type,
          tagName: element.tagName
        };
      }
    });
    
    return formData;
  }

  restoreFormData(formData) {
    if (!formData) return;
    
    Object.entries(formData).forEach(([key, data]) => {
      const element = document.querySelector(`[name="${key}"], #${key}`);
      if (element && element.value !== data.value) {
        element.value = data.value;
        
        // Trigger change event
        element.dispatchEvent(new Event('change', { bubbles: true }));
      }
    });
  }

  restoreFocus(elementInfo) {
    let element = null;
    
    if (elementInfo.id) {
      element = document.getElementById(elementInfo.id);
    } else if (elementInfo.className) {
      element = document.querySelector(`.${elementInfo.className.split(' ')[0]}`);
    } else {
      element = document.querySelector(elementInfo.tagName);
    }
    
    if (element) {
      setTimeout(() => {
        element.focus();
        
        // Restore text selection if applicable
        if (elementInfo.selectionStart !== undefined && element.setSelectionRange) {
          element.setSelectionRange(elementInfo.selectionStart, elementInfo.selectionEnd);
        }
      }, 100);
    }
  }

  fadeOutNonEssentials() {
    return new Promise((resolve) => {
      const nonEssentialElements = document.querySelectorAll('.fade-on-hidden, .advertisement, .sidebar');
      
      nonEssentialElements.forEach(element => {
        element.style.transition = 'opacity 0.3s ease-out';
        element.style.opacity = '0.3';
      });
      
      setTimeout(resolve, 300);
    });
  }

  fadeInElements() {
    return new Promise((resolve) => {
      const elementsToFadeIn = document.querySelectorAll('[style*="opacity"]');
      
      elementsToFadeIn.forEach(element => {
        element.style.transition = 'opacity 0.5s ease-in';
        element.style.opacity = '1';
      });
      
      setTimeout(resolve, 500);
    });
  }

  showWelcomeBackAnimation() {
    return new Promise((resolve) => {
      const welcomeElement = document.createElement('div');
      welcomeElement.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%) scale(0);
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        padding: 20px 30px;
        border-radius: 10px;
        font-family: Arial, sans-serif;
        font-size: 18px;
        font-weight: bold;
        z-index: 1000000;
        box-shadow: 0 10px 25px rgba(0,0,0,0.3);
        transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
      `;
      welcomeElement.textContent = 'Welcome back! 👋';
      
      document.body.appendChild(welcomeElement);
      
      // Animate in
      setTimeout(() => {
        welcomeElement.style.transform = 'translate(-50%, -50%) scale(1)';
      }, 50);
      
      // Animate out
      setTimeout(() => {
        welcomeElement.style.transform = 'translate(-50%, -50%) scale(0)';
        setTimeout(() => {
          welcomeElement.remove();
          resolve();
        }, 400);
      }, 2000);
    });
  }

  pauseAllAnimations() {
    return new Promise((resolve) => {
      // Store current animations
      const runningAnimations = document.getAnimations();
      
      runningAnimations.forEach((animation, index) => {
        this.animations.set(index, {
          animation,
          playState: animation.playState,
          currentTime: animation.currentTime
        });
        
        animation.pause();
      });
      
      console.log(`Paused ${runningAnimations.length} animations`);
      resolve();
    });
  }

  resumeAllAnimations() {
    return new Promise((resolve) => {
      this.animations.forEach((data, index) => {
        try {
          if (data.playState === 'running') {
            data.animation.currentTime = data.currentTime;
            data.animation.play();
          }
        } catch (error) {
          console.warn('Could not resume animation:', error);
        }
      });
      
      this.animations.clear();
      console.log('Resumed animations');
      resolve();
    });
  }

  saveUserProgress() {
    return new Promise((resolve) => {
      // Collect progress data
      const progressData = {
        timestamp: Date.now(),
        url: window.location.href,
        scrollProgress: (window.pageYOffset / (document.body.scrollHeight - window.innerHeight)) * 100,
        formProgress: this.calculateFormProgress(),
        timeOnPage: Date.now() - performance.timing.navigationStart,
        interactionCount: this.getInteractionCount()
      };
      
      // Save to localStorage
      localStorage.setItem('userProgress', JSON.stringify(progressData));
      
      // Optionally send to server
      this.sendProgressToServer(progressData);
      
      console.log('User progress saved');
      resolve();
    });
  }

  calculateFormProgress() {
    let totalFields = 0;
    let filledFields = 0;
    
    document.querySelectorAll('input, textarea, select').forEach(field => {
      if (field.type !== 'hidden' && field.type !== 'submit') {
        totalFields++;
        if (field.value.trim()) {
          filledFields++;
        }
      }
    });
    
    return totalFields > 0 ? (filledFields / totalFields) * 100 : 0;
  }

  getInteractionCount() {
    // This would be tracked by your analytics system
    return parseInt(sessionStorage.getItem('interactionCount') || '0');
  }

  async sendProgressToServer(progressData) {
    try {
      await fetch('/api/user/progress', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(progressData),
        keepalive: true
      });
    } catch (error) {
      console.error('Failed to send progress to server:', error);
    }
  }

  refreshStaleData(timeAway) {
    return new Promise(async (resolve) => {
      // Define refresh thresholds
      const refreshThresholds = {
        light: 300000,    // 5 minutes
        moderate: 600000, // 10 minutes
        aggressive: 1800000 // 30 minutes
      };
      
      let refreshLevel = 'none';
      
      if (timeAway > refreshThresholds.aggressive) {
        refreshLevel = 'aggressive';
      } else if (timeAway > refreshThresholds.moderate) {
        refreshLevel = 'moderate';
      } else if (timeAway > refreshThresholds.light) {
        refreshLevel = 'light';
      }
      
      if (refreshLevel !== 'none') {
        console.log(`Refreshing stale data (level: ${refreshLevel})`);
        await this.performDataRefresh(refreshLevel);
      }
      
      resolve();
    });
  }

  async performDataRefresh(level) {
    const refreshTasks = {
      light: ['notifications', 'status'],
      moderate: ['notifications', 'status', 'content', 'user_data'],
      aggressive: ['notifications', 'status', 'content', 'user_data', 'full_page']
    };
    
    const tasks = refreshTasks[level] || [];
    
    for (const task of tasks) {
      try {
        await this.refreshDataType(task);
      } catch (error) {
        console.error(`Failed to refresh ${task}:`, error);
      }
    }
  }

  async refreshDataType(type) {
    switch (type) {
      case 'notifications':
        // Refresh notification count
        const notificationResponse = await fetch('/api/notifications/count');
        const notificationData = await notificationResponse.json();
        this.updateNotificationUI(notificationData);
        break;
        
      case 'status':
        // Refresh online status, connection status, etc.
        document.dispatchEvent(new CustomEvent('refreshStatus'));
        break;
        
      case 'content':
        // Refresh dynamic content areas
        document.dispatchEvent(new CustomEvent('refreshContent'));
        break;
        
      case 'user_data':
        // Refresh user-specific data
        document.dispatchEvent(new CustomEvent('refreshUserData'));
        break;
        
      case 'full_page':
        // Consider full page refresh for very stale data
        if (confirm('This page has been inactive for a long time. Refresh for latest data?')) {
          window.location.reload();
        }
        break;
    }
  }

  updateNotificationUI(data) {
    const notificationBadge = document.querySelector('.notification-badge');
    if (notificationBadge) {
      notificationBadge.textContent = data.count;
      notificationBadge.style.display = data.count > 0 ? 'block' : 'none';
    }
  }

  // Utility methods
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Public API
  triggerCustomTransition(name, action, delay = 0) {
    this.queueTransition(name, action, delay);
    
    if (!this.transitionState.isTransitioning) {
      this.transitionState.isTransitioning = true;
      this.executeTransitionQueue().then(() => {
        this.transitionState.isTransitioning = false;
      });
    }
  }

  getTransitionState() {
    return {
      isTransitioning: this.transitionState.isTransitioning,
      queueLength: this.transitionState.transitionQueue.length,
      lastChange: this.transitionState.lastVisibilityChange
    };
  }
}

// Initialize smooth transitions
const transitionManager = new SmoothTransitionManager();

// Example of custom transition
document.addEventListener('customDataUpdate', () => {
  transitionManager.triggerCustomTransition('dataUpdate', async () => {
    // Custom transition logic
    const loadingIndicator = document.querySelector('.loading');
    if (loadingIndicator) {
      loadingIndicator.style.display = 'block';
    }
    
    // Simulate data update
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    if (loadingIndicator) {
      loadingIndicator.style.display = 'none';
    }
  });
});

Avoiding Unexpected Behavior Across Browsers

Cross-Browser Compatibility Manager:

class CrossBrowserVisibilityManager {
  constructor() {
    this.browserInfo = this.detectBrowser();
    this.visibilityAPI = this.getVisibilityAPI();
    this.quirksHandler = this.setupQuirksHandler();
    this.eventNormalizer = this.setupEventNormalizer();
    
    this.init();
  }

  detectBrowser() {
    const userAgent = navigator.userAgent;
    const vendor = navigator.vendor;
    
    let browser = 'unknown';
    let version = 'unknown';
    
    if (/Chrome/.test(userAgent) && /Google Inc/.test(vendor)) {
      browser = 'chrome';
      version = userAgent.match(/Chrome\/(\d+)/)?.[1];
    } else if (/Firefox/.test(userAgent)) {
      browser = 'firefox';
      version = userAgent.match(/Firefox\/(\d+)/)?.[1];
    } else if (/Safari/.test(userAgent) && /Apple Computer/.test(vendor)) {
      browser = 'safari';
      version = userAgent.match(/Version\/(\d+)/)?.[1];
    } else if (/Edge/.test(userAgent)) {
      browser = 'edge';
      version = userAgent.match(/Edge\/(\d+)/)?.[1];
    }
    
    return { browser, version: parseInt(version) || 0 };
  }

  getVisibilityAPI() {
    // Cross-browser visibility API detection
    if (typeof document.hidden !== 'undefined') {
      return {
        hidden: 'hidden',
        visibilityState: 'visibilityState',
        visibilityChange: 'visibilitychange'
      };
    } else if (typeof document.webkitHidden !== 'undefined') {
      return {
        hidden: 'webkitHidden',
        visibilityState: 'webkitVisibilityState',
        visibilityChange: 'webkitvisibilitychange'
      };
    } else if (typeof document.mozHidden !== 'undefined') {
      return {
        hidden: 'mozHidden',
        visibilityState: 'mozVisibilityState',
        visibilityChange: 'mozvisibilitychange'
      };
    } else if (typeof document.msHidden !== 'undefined') {
      return {
        hidden: 'msHidden',
        visibilityState: 'msVisibilityState',
        visibilityChange: 'msvisibilitychange'
      };
    }
    
    return null;
  }

  setupQuirksHandler() {
    const quirks = {};
    
    // Safari-specific quirks
    if (this.browserInfo.browser === 'safari') {
      quirks.safariBackgroundThrottle = true;
      quirks.safariBlurDelay = 100; // Safari has delayed blur events
      quirks.safariVisibilityInconsistent = this.browserInfo.version < 14;
    }
    
    // Firefox-specific quirks
    if (this.browserInfo.browser === 'firefox') {
      quirks.firefoxMinimizeDetection = this.browserInfo.version < 90;
      quirks.firefoxFocusEvents = true;
    }
    
    // Chrome-specific quirks
    if (this.browserInfo.browser === 'chrome') {
      quirks.chromeBackgroundTabThrottling = this.browserInfo.version >= 57;
      quirks.chromeAgressiveThrottling = this.browserInfo.version >= 88;
    }
    
    // Mobile browser quirks
    if (/Mobile|Android|iPhone|iPad/.test(navigator.userAgent)) {
      quirks.mobileAppSwitch = true;
      quirks.mobileLockScreen = true;
      quirks.mobileMemoryManagement = true;
    }
    
    return quirks;
  }

  setupEventNormalizer() {
    return {
      normalizeVisibilityEvent: (event) => {
        return {
          type: 'visibilitychange',
          hidden: document[this.visibilityAPI.hidden],
          visibilityState: document[this.visibilityAPI.visibilityState],
          timestamp: Date.now(),
          originalEvent: event,
          browser: this.browserInfo.browser
        };
      },
      
      addBrowserSpecificData: (eventData) => {
        if (this.quirks.safariBackgroundThrottle && eventData.hidden) {
          eventData.safariThrottled = true;
        }
        
        if (this.quirks.chromeAgressiveThrottling && eventData.hidden) {
          eventData.chromeThrottled = true;
        }
        
        return eventData;
      }
    };
  }

  init() {
    if (!this.visibilityAPI) {
      console.warn('Page Visibility API not supported, falling back to focus/blur events');
      this.setupFallbackEvents();
      return;
    }
    
    this.setupVisibilityEvents();
    this.setupBrowserSpecificHandling();
    this.setupFallbackEvents(); // Additional layer for reliability
  }

  setupVisibilityEvents() {
    document.addEventListener(this.visibilityAPI.visibilityChange, (event) => {
      const normalizedEvent = this.eventNormalizer.normalizeVisibilityEvent(event);
      const enhancedEvent = this.eventNormalizer.addBrowserSpecificData(normalizedEvent);
      
      this.handleVisibilityChange(enhancedEvent);
    });
  }

  setupFallbackEvents() {
    // Focus/blur events as fallback
    window.addEventListener('focus', () => {
      const event = {
        type: 'focus',
        hidden: false,
        visibilityState: 'visible',
        timestamp: Date.now(),
        fallback: true,
        browser: this.browserInfo.browser
      };
      
      this.handleVisibilityChange(event);
    });
    
    window.addEventListener('blur', () => {
      // Delay for Safari quirk
      const delay = this.quirks.safariBlurDelay || 0;
      
      setTimeout(() => {
        const event = {
          type: 'blur',
          hidden: true,
          visibilityState: 'hidden',
          timestamp: Date.now(),
          fallback: true,
          browser: this.browserInfo.browser
        };
        
        this.handleVisibilityChange(event);
      }, delay);
    });
    
    // Page hide/show events for mobile
    if (this.quirks.mobileAppSwitch) {
      window.addEventListener('pagehide', () => {
        this.handleVisibilityChange({
          type: 'pagehide',
          hidden: true,
          visibilityState: 'hidden',
          timestamp: Date.now(),
          mobile: true
        });
      });
      
      window.addEventListener('pageshow', () => {
        this.handleVisibilityChange({
          type: 'pageshow',
          hidden: false,
          visibilityState: 'visible',
          timestamp: Date.now(),
          mobile: true
        });
      });
    }
  }

  setupBrowserSpecificHandling() {
    // Safari-specific handling
    if (this.browserInfo.browser === 'safari') {
      this.setupSafariWorkarounds();
    }
    
    // Firefox-specific handling
    if (this.browserInfo.browser === 'firefox') {
      this.setupFirefoxWorkarounds();
    }
    
    // Mobile-specific handling
    if (this.quirks.mobileAppSwitch) {
      this.setupMobileWorkarounds();
    }
  }

  setupSafariWorkarounds() {
    // Safari sometimes doesn't fire visibility events on tab switch
    let safariCheckInterval;
    
    const startSafariCheck = () => {
      safariCheckInterval = setInterval(() => {
        const actuallyHidden = document[this.visibilityAPI.hidden];
        const lastKnownState = this.lastVisibilityState;
        
        if (actuallyHidden !== lastKnownState) {
          console.log('Safari visibility state correction needed');
          this.handleVisibilityChange({
            type: 'safari-correction',
            hidden: actuallyHidden,
            visibilityState: actuallyHidden ? 'hidden' : 'visible',
            timestamp: Date.now(),
            correction: true
          });
        }
      }, 1000);
    };
    
    const stopSafariCheck = () => {
      if (safariCheckInterval) {
        clearInterval(safariCheckInterval);
        safariCheckInterval = null;
      }
    };
    
    // Start checking when potentially problematic
    document.addEventListener(this.visibilityAPI.visibilityChange, (event) => {
      if (document[this.visibilityAPI.hidden]) {
        startSafariCheck();
      } else {
        stopSafariCheck();
      }
    });
  }

  setupFirefoxWorkarounds() {
    // Firefox sometimes has issues with minimized windows
    if (this.quirks.firefoxMinimizeDetection) {
      let firefoxState = 'visible';
      
      window.addEventListener('resize', () => {
        // Detect minimization
        if (window.outerHeight === 0 || window.outerWidth === 0) {
          if (firefoxState !== 'minimized') {
            firefoxState = 'minimized';
            this.handleVisibilityChange({
              type: 'firefox-minimize',
              hidden: true,
              visibilityState: 'hidden',
              timestamp: Date.now(),
              minimized: true
            });
          }
        } else if (firefoxState === 'minimized') {
          firefoxState = 'visible';
          this.handleVisibilityChange({
            type: 'firefox-restore',
            hidden: false,
            visibilityState: 'visible',
            timestamp: Date.now(),
            restored: true
          });
        }
      });
    }
  }

  setupMobileWorkarounds() {
    // Mobile devices have additional complexity
    let mobileState = 'active';
    
    // Handle orientation changes
    window.addEventListener('orientationchange', () => {
      setTimeout(() => {
        // Check if still visible after orientation change
        if (!document[this.visibilityAPI.hidden]) {
          this.handleVisibilityChange({
            type: 'orientation-change',
            hidden: false,
            visibilityState: 'visible',
            timestamp: Date.now(),
            orientationChange: true
          });
        }
      }, 500);
    });
    
    // Handle app lifecycle events
    document.addEventListener('resume', () => {
      this.handleVisibilityChange({
        type: 'app-resume',
        hidden: false,
        visibilityState: 'visible',
        timestamp: Date.now(),
        appResume: true
      });
    });
    
    document.addEventListener('pause', () => {
      this.handleVisibilityChange({
        type: 'app-pause',
        hidden: true,
        visibilityState: 'hidden',
        timestamp: Date.now(),
        appPause: true
      });
    });
  }

  handleVisibilityChange(eventData) {
    this.lastVisibilityState = eventData.hidden;
    
    // Log browser-specific information
    console.log('Visibility change:', {
      browser: this.browserInfo.browser,
      version: this.browserInfo.version,
      event: eventData,
      quirks: this.quirks
    });
    
    // Emit normalized event
    document.dispatchEvent(new CustomEvent('normalizedVisibilityChange', {
      detail: eventData
    }));
  }

  // Public API for checking browser compatibility
  getBrowserCompatibility() {
    return {
      browserInfo: this.browserInfo,
      hasVisibilityAPI: !!this.visibilityAPI,
      quirks: this.quirks,
      supportLevel: this.calculateSupportLevel()
    };
  }

  calculateSupportLevel() {
    if (!this.visibilityAPI) return 'basic';
    
    let score = 10;
    
    // Deduct points for known issues
    if (this.quirks.safariVisibilityInconsistent) score -= 2;
    if (this.quirks.firefoxMinimizeDetection) score -= 1;
    if (this.quirks.mobileMemoryManagement) score -= 1;
    
    if (score >= 9) return 'excellent';
    if (score >= 7) return 'good';
    if (score >= 5) return 'fair';
    return 'basic';
  }
}

// Initialize cross-browser compatibility
const crossBrowserManager = new CrossBrowserVisibilityManager();

// Use normalized events
document.addEventListener('normalizedVisibilityChange', (event) => {
  const { detail } = event;
  
  if (detail.hidden) {
    console.log('Tab hidden (normalized):', detail);
    // Handle tab hidden with browser-specific context
  } else {
    console.log('Tab visible (normalized):', detail);
    // Handle tab visible with browser-specific context
  }
});

// Check compatibility
const compatibility = crossBrowserManager.getBrowserCompatibility();
console.log('Browser compatibility:', compatibility);

Conclusion

Implementing browser tab change detection with JavaScript’s Page Visibility API opens up powerful possibilities for creating more responsive, efficient, and user-friendly web applications. From automatically pausing media when users switch tabs to implementing sophisticated security measures that protect sensitive data during inactivity, tab visibility detection has become an essential tool in modern web development.

Recap of Key Techniques and Implementation Tips

Core Implementation Strategies:

  • Use the Page Visibility API as your primary detection method, with focus/blur events as fallback
  • Implement cross-browser compatibility layers to handle vendor prefixes and browser quirks
  • Create debounced event handlers to prevent rapid-fire state changes from causing performance issues
  • Build modular systems that can be easily extended with additional functionality

Performance Optimization Techniques:

  • Pause resource-intensive operations like animations, polling, and media playback when tabs are hidden
  • Implement intelligent caching strategies that adapt to user attention patterns
  • Use battery-aware optimizations on mobile devices to preserve device power
  • Create smooth transition effects that enhance rather than interrupt user experience

Security and Session Management:

  • Implement graduated security measures based on inactivity duration and sensitivity levels
  • Use tab visibility data to inform session timeout calculations and warnings
  • Automatically mask sensitive information when tabs lose focus
  • Provide clear user feedback about security actions and session status

Final Thoughts on Ethical Use and User Trust

When implementing tab visibility detection, always prioritize user privacy and trust. Be transparent about what your application does when tabs are hidden, and provide users with control over these behaviors when possible. Avoid overly aggressive tracking or resource consumption that could negatively impact user experience.

Ethical Guidelines:

  • Clearly communicate in your privacy policy how tab visibility affects application behavior
  • Provide users with options to disable automatic pausing or security features if appropriate
  • Respect user bandwidth and battery life by being conservative with background activity
  • Use tab detection to enhance, not replace, good fundamental application design

Encouragement to Enhance UX by Respecting User Behavior

The most successful implementations of tab visibility detection feel invisible to users—they simply experience a more responsive and intuitive application. Focus on solving real user problems rather than implementing features for their own sake.

Consider tab switching as a natural part of modern browsing behavior, not an interruption to manage. Your application should gracefully adapt to user attention patterns while preserving their work and providing seamless transitions between focused and background states.

Start with simple implementations like pausing media or saving form data, then gradually add more sophisticated features as you understand your users’ specific needs and workflows. Remember that the goal is always to create better user experiences, not to exert control over user behavior.

By thoughtfully implementing tab visibility detection, you’ll create applications that feel more alive and responsive while respecting the multi-tasking nature of modern web usage. The techniques covered in this guide provide a solid foundation for building these enhanced experiences while maintaining performance, security, and user trust.

Comments (0)

Comment


Note: All Input Fields are required.