UNPKG

solo-tab-enforcer

Version:

Cross-browser solution for enforcing single tab usage in web applications

1,401 lines (1,228 loc) 35 kB
/** * Solo Tab Enforcer - Cross-Browser Tab Management * Version: 1.0.1 * License: MIT * * A comprehensive solution for enforcing single tab usage across all browsers. * Uses native browser APIs with fallback strategies for maximum compatibility. */ (function(global, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { global.SoloTabEnforcer = factory(); } }(typeof window !== 'undefined' ? window : this, function() { 'use strict'; // src/utils/CrossBrowserCompat.js /** * Cross-browser compatibility utilities */ class CrossBrowserCompat { /** * Check if running in browser environment */ static isBrowser() { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * Check if running in Node.js environment */ static isNode() { return ( typeof process !== 'undefined' && process.versions && process.versions.node ); } /** * Get browser information */ static getBrowserInfo() { if (!this.isBrowser()) { return { name: 'unknown', version: 'unknown' }; } const ua = navigator.userAgent; let browserName = 'unknown'; let browserVersion = 'unknown'; // Chrome if (ua.indexOf('Chrome') > -1) { browserName = 'chrome'; browserVersion = ua.match(/Chrome\/(\d+)/)?.[1] || 'unknown'; } // Firefox else if (ua.indexOf('Firefox') > -1) { browserName = 'firefox'; browserVersion = ua.match(/Firefox\/(\d+)/)?.[1] || 'unknown'; } // Safari else if (ua.indexOf('Safari') > -1) { browserName = 'safari'; browserVersion = ua.match(/Version\/(\d+)/)?.[1] || 'unknown'; } // Edge else if (ua.indexOf('Edge') > -1) { browserName = 'edge'; browserVersion = ua.match(/Edge\/(\d+)/)?.[1] || 'unknown'; } // IE else if (ua.indexOf('MSIE') > -1 || ua.indexOf('Trident') > -1) { browserName = 'ie'; browserVersion = ua.match(/(MSIE|rv:)(\d+)/)?.[2] || 'unknown'; } return { name: browserName, version: browserVersion }; } /** * Check feature support */ static checkFeatureSupport() { if (!this.isBrowser()) { return { localStorage: false, sessionStorage: false, broadcastChannel: false, visibilityAPI: false, storageEvents: false, }; } return { localStorage: typeof localStorage !== 'undefined', sessionStorage: typeof sessionStorage !== 'undefined', broadcastChannel: typeof BroadcastChannel !== 'undefined', visibilityAPI: typeof document.visibilityState !== 'undefined', storageEvents: typeof window.addEventListener !== 'undefined', }; } /** * Polyfill for older browsers */ static applyPolyfills() { if (!this.isBrowser()) { return; } // Polyfill for Object.assign (IE) if (!Object.assign) { Object.assign = function (target, ...sources) { if (target == null) { throw new TypeError('Cannot convert undefined or null to object'); } const to = Object(target); for (let i = 0; i < sources.length; i++) { const source = sources[i]; if (source != null) { for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { to[key] = source[key]; } } } } return to; }; } // Polyfill for Array.prototype.includes (IE) if (!Array.prototype.includes) { Array.prototype.includes = function (searchElement, fromIndex) { if (this == null) { throw new TypeError( 'Array.prototype.includes called on null or undefined' ); } const o = Object(this); const len = parseInt(o.length) || 0; if (len === 0) { return false; } const n = parseInt(fromIndex) || 0; let k = n >= 0 ? n : Math.max(len + n, 0); while (k < len) { if (o[k] === searchElement) { return true; } k++; } return false; }; } // Polyfill for String.prototype.includes (IE) if (!String.prototype.includes) { String.prototype.includes = function (search, start) { if (typeof start !== 'number') { start = 0; } if (start + search.length > this.length) { return false; } else { return this.indexOf(search, start) !== -1; } }; } } /** * Get storage implementation based on browser support */ static getStorageImplementation() { if (!this.isBrowser()) { return null; } const support = this.checkFeatureSupport(); if (support.localStorage) { return localStorage; } else if (support.sessionStorage) { return sessionStorage; } else { // Fallback to cookie-based storage return this.getCookieStorage(); } } /** * Cookie-based storage fallback */ static getCookieStorage() { return { getItem: function (key) { const name = key + '='; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1); } if (c.indexOf(name) === 0) { return c.substring(name.length, c.length); } } return null; }, setItem: function (key, value) { document.cookie = key + '=' + value + ';path=/'; }, removeItem: function (key) { document.cookie = key + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/'; }, }; } /** * Generate unique ID across browsers */ static generateUniqueId() { if ( this.isBrowser() && typeof crypto !== 'undefined' && crypto.getRandomValues ) { // Use crypto API for better randomness const array = new Uint32Array(2); crypto.getRandomValues(array); return array[0].toString(36) + array[1].toString(36); } else { // Fallback to Math.random return Date.now().toString(36) + Math.random().toString(36).substr(2); } } /** * Add event listener with cross-browser support */ static addEventListener(element, event, handler, options) { if (!this.isBrowser()) { return; } if (element.addEventListener) { element.addEventListener(event, handler, options); } else if (element.attachEvent) { // IE8 and below element.attachEvent('on' + event, handler); } else { // Very old browsers element['on' + event] = handler; } } /** * Remove event listener with cross-browser support */ static removeEventListener(element, event, handler, options) { if (!this.isBrowser()) { return; } if (element.removeEventListener) { element.removeEventListener(event, handler, options); } else if (element.detachEvent) { // IE8 and below element.detachEvent('on' + event, handler); } else { // Very old browsers element['on' + event] = null; } } } // src/adapters/BrowserAdapter.js /** * Browser-specific adapter for different browsers */ class BrowserAdapter { constructor() { this.browserInfo = this.getBrowserInfo(); this.features = this.detectFeatures(); } /** * Get browser information */ getBrowserInfo() { if (typeof navigator === 'undefined') { return { name: 'unknown', version: 'unknown' }; } const ua = navigator.userAgent; const browsers = [ { name: 'chrome', pattern: /Chrome\/(\d+)/ }, { name: 'firefox', pattern: /Firefox\/(\d+)/ }, { name: 'safari', pattern: /Version\/(\d+).*Safari/ }, { name: 'edge', pattern: /Edge\/(\d+)/ }, { name: 'ie', pattern: /(MSIE|rv:)(\d+)/ }, ]; for (const browser of browsers) { const match = ua.match(browser.pattern); if (match) { return { name: browser.name, version: match[1] || match[2] || 'unknown', }; } } return { name: 'unknown', version: 'unknown' }; } /** * Detect browser features */ detectFeatures() { if (typeof window === 'undefined') { return {}; } return { localStorage: typeof localStorage !== 'undefined', sessionStorage: typeof sessionStorage !== 'undefined', broadcastChannel: typeof BroadcastChannel !== 'undefined', visibilityAPI: typeof document.visibilityState !== 'undefined', storageEvents: typeof window.addEventListener !== 'undefined', webWorkers: typeof Worker !== 'undefined', serviceWorkers: 'serviceWorker' in navigator, indexedDB: typeof indexedDB !== 'undefined', crypto: typeof crypto !== 'undefined' && typeof crypto.getRandomValues !== 'undefined', }; } /** * Get optimal storage method for current browser */ getStorageMethod() { if (this.features.localStorage) { return { type: 'localStorage', get: (key) => localStorage.getItem(key), set: (key, value) => localStorage.setItem(key, value), remove: (key) => localStorage.removeItem(key), }; } else if (this.features.sessionStorage) { return { type: 'sessionStorage', get: (key) => sessionStorage.getItem(key), set: (key, value) => sessionStorage.setItem(key, value), remove: (key) => sessionStorage.removeItem(key), }; } else { // Fallback to cookies return { type: 'cookies', get: (key) => this.getCookie(key), set: (key, value) => this.setCookie(key, value), remove: (key) => this.removeCookie(key), }; } } /** * Get optimal communication method for current browser */ getCommunicationMethod() { if (this.features.broadcastChannel) { return { type: 'broadcastChannel', create: (channel) => new BroadcastChannel(channel), send: (channel, data) => channel.postMessage(data), close: (channel) => channel.close(), }; } else if (this.features.storageEvents) { return { type: 'storageEvents', create: (key) => ({ key }), send: (context, data) => { const event = new CustomEvent('storage', { detail: { key: context.key, data }, }); window.dispatchEvent(event); }, close: () => {}, }; } else { // Fallback to polling return { type: 'polling', create: (key) => ({ key }), send: () => {}, close: () => {}, }; } } /** * Get visibility detection method */ getVisibilityMethod() { if (this.features.visibilityAPI) { return { type: 'visibilityAPI', isVisible: () => document.visibilityState === 'visible', onVisibilityChange: (callback) => { document.addEventListener('visibilitychange', callback); return () => document.removeEventListener('visibilitychange', callback); }, }; } else { // Fallback to focus/blur events return { type: 'focusBlur', isVisible: () => (document.hasFocus ? document.hasFocus() : true), onVisibilityChange: (callback) => { const focusHandler = () => callback({ target: { visibilityState: 'visible' } }); const blurHandler = () => callback({ target: { visibilityState: 'hidden' } }); window.addEventListener('focus', focusHandler); window.addEventListener('blur', blurHandler); return () => { window.removeEventListener('focus', focusHandler); window.removeEventListener('blur', blurHandler); }; }, }; } } /** * Cookie utilities for fallback */ getCookie(name) { if (typeof document === 'undefined') return null; const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { return parts.pop().split(';').shift(); } return null; } setCookie(name, value, days = 1) { if (typeof document === 'undefined') return; const expires = new Date(); expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`; } removeCookie(name) { if (typeof document === 'undefined') return; document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`; } /** * Generate unique ID using best available method */ generateUniqueId() { if (this.features.crypto) { const array = new Uint32Array(2); crypto.getRandomValues(array); return array[0].toString(36) + array[1].toString(36); } else { return Date.now().toString(36) + Math.random().toString(36).substr(2); } } /** * Get browser-specific optimizations */ getOptimizations() { const optimizations = { checkInterval: 1000, heartbeatInterval: 5000, timeoutMs: 10000, }; // Browser-specific optimizations switch (this.browserInfo.name) { case 'chrome': optimizations.checkInterval = 500; optimizations.heartbeatInterval = 3000; break; case 'firefox': optimizations.checkInterval = 750; optimizations.heartbeatInterval = 4000; break; case 'safari': optimizations.checkInterval = 1500; optimizations.heartbeatInterval = 6000; break; case 'ie': optimizations.checkInterval = 2000; optimizations.heartbeatInterval = 8000; optimizations.timeoutMs = 15000; break; } return optimizations; } } // src/strategies/FallbackStrategy.js /** * Fallback strategy for browsers with limited feature support */ class FallbackStrategy { constructor(options = {}) { this.options = { pollInterval: 2000, maxRetries: 3, retryDelay: 1000, ...options, }; this.isActive = false; this.pollTimer = null; this.retryCount = 0; } /** * Initialize fallback strategy */ init() { this.startPolling(); } /** * Start polling for tab conflicts */ startPolling() { this.pollTimer = setInterval(() => { this.checkForConflicts(); }, this.options.pollInterval); } /** * Check for conflicts using document title manipulation */ checkForConflicts() { try { const originalTitle = document.title; const marker = '__tab_check__'; // Try to set a marker in the title document.title = marker; // Check if title was actually changed if (document.title === marker) { // We have control, restore original title document.title = originalTitle; this.handleTabActivated(); } else { // Another tab might have control this.handleTabConflict(); } } catch (error) { this.handleError(error); } } /** * Use URL hash for tab coordination */ useHashStrategy() { const tabId = this.generateTabId(); const originalHash = window.location.hash; // Set our tab ID in hash window.location.hash = `#tab_${tabId}`; // Check if hash was actually set setTimeout(() => { if (window.location.hash === `#tab_${tabId}`) { this.handleTabActivated(); } else { this.handleTabConflict(); } // Restore original hash window.location.hash = originalHash; }, 100); } /** * Use window.name for tab identification */ useWindowNameStrategy() { const tabId = this.generateTabId(); const originalName = window.name; if (!window.name || window.name.indexOf('tab_') !== 0) { window.name = `tab_${tabId}`; this.handleTabActivated(); } else if (window.name !== `tab_${tabId}`) { this.handleTabConflict(); } } /** * Use global variable strategy */ useGlobalVariableStrategy() { const tabId = this.generateTabId(); if (typeof window.__tabEnforcer === 'undefined') { window.__tabEnforcer = { tabId: tabId, timestamp: Date.now(), }; this.handleTabActivated(); } else { const timeDiff = Date.now() - window.__tabEnforcer.timestamp; if (timeDiff > 5000) { // Assume the other tab is dead window.__tabEnforcer = { tabId: tabId, timestamp: Date.now(), }; this.handleTabActivated(); } else if (window.__tabEnforcer.tabId !== tabId) { this.handleTabConflict(); } } } /** * Use iframe communication strategy */ useIframeStrategy() { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = 'about:blank'; document.body.appendChild(iframe); try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const tabId = this.generateTabId(); if (!iframeDoc.title) { iframeDoc.title = `tab_${tabId}`; this.handleTabActivated(); } else if (iframeDoc.title !== `tab_${tabId}`) { this.handleTabConflict(); } } catch (error) { // Cross-origin restrictions, cleanup document.body.removeChild(iframe); this.handleError(error); } } /** * Handle tab activation */ handleTabActivated() { this.isActive = true; this.retryCount = 0; if (this.options.onTabActivated) { this.options.onTabActivated(); } } /** * Handle tab conflict */ handleTabConflict() { this.isActive = false; if (this.options.onTabConflict) { this.options.onTabConflict(); } } /** * Handle errors with retry logic */ handleError(error) { console.warn('Fallback strategy error:', error); if (this.retryCount < this.options.maxRetries) { this.retryCount++; setTimeout(() => { this.checkForConflicts(); }, this.options.retryDelay); } } /** * Generate simple tab ID */ generateTabId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); } /** * Destroy fallback strategy */ destroy() { if (this.pollTimer) { clearInterval(this.pollTimer); } // Cleanup global variables } } // src/core/TabEnforcer.js /** * Solo Tab Enforcer - Core Module * Cross-browser solution for enforcing single tab usage */ class TabEnforcer { constructor(options = {}) { this.options = { storageKey: 'solo-tab-enforcer', checkInterval: 1000, warningMessage: 'Another tab is already open. Please close other tabs to continue.', redirectUrl: null, allowMultipleTabs: false, debug: false, onTabConflict: null, onTabActivated: null, onTabDeactivated: null, useVisibilityAPI: true, useBroadcastChannel: true, useStorageEvents: true, tabTimeoutMs: 5000, ...options, }; this.tabId = this.generateTabId(); this.isActive = false; this.isInitialized = false; this.broadcastChannel = null; this.storageEventListener = null; this.visibilityChangeListener = null; this.beforeUnloadListener = null; this.focusListener = null; this.blurListener = null; this.checkTimer = null; this.heartbeatTimer = null; this.supportedFeatures = this.detectFeatures(); this.log('TabEnforcer initialized with options:', this.options); } /** * Generate unique tab identifier */ generateTabId() { return `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Detect browser features */ detectFeatures() { return { broadcastChannel: typeof BroadcastChannel !== 'undefined', visibilityAPI: typeof document.visibilityState !== 'undefined', localStorage: typeof localStorage !== 'undefined', sessionStorage: typeof sessionStorage !== 'undefined', storageEvents: typeof window.addEventListener !== 'undefined', }; } /** * Initialize the tab enforcer */ init() { if (this.isInitialized) { this.log('TabEnforcer already initialized'); return; } this.log('Initializing TabEnforcer...'); if (this.options.allowMultipleTabs) { this.log('Multiple tabs allowed, enforcer disabled'); return; } this.setupEventListeners(); this.registerTab(); this.startHeartbeat(); this.startTabCheck(); this.isInitialized = true; this.log('TabEnforcer initialized successfully'); } /** * Setup event listeners for different browser APIs */ setupEventListeners() { // BroadcastChannel for modern browsers if ( this.options.useBroadcastChannel && this.supportedFeatures.broadcastChannel ) { this.setupBroadcastChannel(); } // Storage events for cross-tab communication if (this.options.useStorageEvents && this.supportedFeatures.storageEvents) { this.setupStorageEvents(); } // Visibility API for tab focus detection if (this.options.useVisibilityAPI && this.supportedFeatures.visibilityAPI) { this.setupVisibilityAPI(); } // Window focus/blur events (fallback) this.setupFocusEvents(); // Cleanup on page unload this.setupUnloadEvents(); } /** * Setup BroadcastChannel communication */ setupBroadcastChannel() { try { this.broadcastChannel = new BroadcastChannel(this.options.storageKey); this.broadcastChannel.onmessage = (event) => { this.handleBroadcastMessage(event.data); }; this.log('BroadcastChannel initialized'); } catch (error) { this.log('BroadcastChannel failed to initialize:', error); } } /** * Setup storage events for cross-tab communication */ setupStorageEvents() { this.storageEventListener = (event) => { if (event.key === this.options.storageKey) { this.handleStorageEvent(event); } }; window.addEventListener('storage', this.storageEventListener); this.log('Storage events initialized'); } /** * Setup Visibility API */ setupVisibilityAPI() { this.visibilityChangeListener = () => { if (document.visibilityState === 'visible') { this.handleTabActivated(); } else { this.handleTabDeactivated(); } }; document.addEventListener( 'visibilitychange', this.visibilityChangeListener ); this.log('Visibility API initialized'); } /** * Setup focus/blur events */ setupFocusEvents() { this.focusListener = () => this.handleTabActivated(); this.blurListener = () => this.handleTabDeactivated(); window.addEventListener('focus', this.focusListener); window.addEventListener('blur', this.blurListener); this.log('Focus events initialized'); } /** * Setup unload events */ setupUnloadEvents() { this.beforeUnloadListener = () => { this.unregisterTab(); }; window.addEventListener('beforeunload', this.beforeUnloadListener); window.addEventListener('unload', this.beforeUnloadListener); this.log('Unload events initialized'); } /** * Register current tab */ registerTab() { const tabData = { id: this.tabId, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, isActive: document.visibilityState === 'visible', }; this.setStorageData(tabData); this.broadcastMessage({ type: 'tab-registered', tabId: this.tabId }); this.log('Tab registered:', this.tabId); } /** * Unregister current tab */ unregisterTab() { this.removeStorageData(); this.broadcastMessage({ type: 'tab-unregistered', tabId: this.tabId }); this.log('Tab unregistered:', this.tabId); } /** * Start heartbeat to maintain tab presence */ startHeartbeat() { this.heartbeatTimer = setInterval(() => { this.updateHeartbeat(); }, this.options.checkInterval); } /** * Update heartbeat timestamp */ updateHeartbeat() { const existingData = this.getStorageData(); if (existingData && existingData.id === this.tabId) { existingData.timestamp = Date.now(); this.setStorageData(existingData); } } /** * Start tab checking routine */ startTabCheck() { this.checkTimer = setInterval(() => { this.checkForConflicts(); }, this.options.checkInterval); } /** * Check for tab conflicts */ checkForConflicts() { const existingData = this.getStorageData(); if (!existingData) { // No existing tab, register this one this.registerTab(); return; } // Check if existing tab is still alive const timeDiff = Date.now() - existingData.timestamp; if (timeDiff > this.options.tabTimeoutMs) { // Existing tab is dead, take over this.registerTab(); return; } // Check if this is a different tab if (existingData.id !== this.tabId) { this.handleTabConflict(existingData); } } /** * Handle tab conflict */ handleTabConflict(existingTab) { this.log('Tab conflict detected:', existingTab); if (this.options.onTabConflict) { this.options.onTabConflict(existingTab); } else { this.showDefaultWarning(); } } /** * Show default warning message */ showDefaultWarning() { if (this.options.redirectUrl) { window.location.href = this.options.redirectUrl; } else { alert(this.options.warningMessage); window.close(); } } /** * Handle tab activation */ handleTabActivated() { this.isActive = true; this.log('Tab activated'); if (this.options.onTabActivated) { this.options.onTabActivated(); } // Re-register tab when activated this.registerTab(); } /** * Handle tab deactivation */ handleTabDeactivated() { this.isActive = false; this.log('Tab deactivated'); if (this.options.onTabDeactivated) { this.options.onTabDeactivated(); } } /** * Handle broadcast messages */ handleBroadcastMessage(data) { this.log('Received broadcast message:', data); switch (data.type) { case 'tab-registered': if (data.tabId !== this.tabId) { this.checkForConflicts(); } break; case 'tab-unregistered': // Another tab closed, we might be able to take over if (data.tabId !== this.tabId) { setTimeout(() => this.registerTab(), 100); } break; } } /** * Handle storage events */ handleStorageEvent(event) { this.log('Storage event:', event); if (event.newValue && event.newValue !== event.oldValue) { const newData = JSON.parse(event.newValue); if (newData.id !== this.tabId) { this.checkForConflicts(); } } } /** * Broadcast message to other tabs */ broadcastMessage(data) { if (this.broadcastChannel) { try { this.broadcastChannel.postMessage(data); } catch (error) { this.log('Failed to broadcast message:', error); } } } /** * Get data from storage */ getStorageData() { try { const data = localStorage.getItem(this.options.storageKey); return data ? JSON.parse(data) : null; } catch (error) { this.log('Failed to get storage data:', error); return null; } } /** * Set data to storage */ setStorageData(data) { try { localStorage.setItem(this.options.storageKey, JSON.stringify(data)); } catch (error) { this.log('Failed to set storage data:', error); } } /** * Remove data from storage */ removeStorageData() { try { localStorage.removeItem(this.options.storageKey); } catch (error) { this.log('Failed to remove storage data:', error); } } /** * Get current tab information */ getTabInfo() { return { id: this.tabId, isActive: this.isActive, isInitialized: this.isInitialized, supportedFeatures: this.supportedFeatures, options: this.options, }; } /** * Destroy the tab enforcer */ destroy() { this.log('Destroying TabEnforcer...'); // Clear timers if (this.checkTimer) { clearInterval(this.checkTimer); } if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); } // Remove event listeners if (this.storageEventListener) { window.removeEventListener('storage', this.storageEventListener); } if (this.visibilityChangeListener) { document.removeEventListener( 'visibilitychange', this.visibilityChangeListener ); } if (this.beforeUnloadListener) { window.removeEventListener('beforeunload', this.beforeUnloadListener); window.removeEventListener('unload', this.beforeUnloadListener); } if (this.focusListener) { window.removeEventListener('focus', this.focusListener); } if (this.blurListener) { window.removeEventListener('blur', this.blurListener); } // Close broadcast channel if (this.broadcastChannel) { this.broadcastChannel.close(); } // Unregister tab this.unregisterTab(); this.isInitialized = false; this.log('TabEnforcer destroyed'); } /** * Log messages if debug is enabled */ log(...args) { if (this.options.debug) { console.log(`[TabEnforcer:${this.tabId}]`, ...args); } } } // src/index.js /** * Solo Tab Enforcer - Main Entry Point * Cross-browser solution for enforcing single tab usage */ // Import core modules /** * Main SoloTabEnforcer class that orchestrates all components */ class SoloTabEnforcer { constructor(options = {}) { // Apply polyfills for older browsers // Initialize browser adapter this.adapter = new BrowserAdapter(); // Merge options with browser-specific optimizations this.options = { ...this.adapter.getOptimizations(), ...options, }; // Initialize core enforcer this.enforcer = new TabEnforcer(this.options); // Initialize fallback strategy for unsupported browsers this.fallbackStrategy = new FallbackStrategy({ ...this.options, onTabActivated: () => this.handleTabActivated(), onTabConflict: () => this.handleTabConflict(), }); this.isUsingFallback = false; } /** * Initialize the tab enforcer */ init() { // Check if we have modern browser support if ( this.adapter.features.localStorage && this.adapter.features.storageEvents ) { this.enforcer.init(); } else { // Use fallback strategy this.isUsingFallback = true; this.fallbackStrategy.init(); } } /** * Handle tab activation */ handleTabActivated() { if (this.options.onTabActivated) { this.options.onTabActivated(); } } /** * Handle tab conflict */ handleTabConflict() { if (this.options.onTabConflict) { this.options.onTabConflict(); } } /** * Get current tab information */ getTabInfo() { const baseInfo = { browserInfo: this.adapter.browserInfo, features: this.adapter.features, isUsingFallback: this.isUsingFallback, }; if (this.isUsingFallback) { return { ...baseInfo, isActive: this.fallbackStrategy.isActive, }; } else { return { ...baseInfo, ...this.enforcer.getTabInfo(), }; } } /** * Check if multiple tabs are allowed */ areMultipleTabsAllowed() { return this.options.allowMultipleTabs; } /** * Enable multiple tabs */ allowMultipleTabs() { this.options.allowMultipleTabs = true; if (!this.isUsingFallback) { this.enforcer.options.allowMultipleTabs = true; } } /** * Disable multiple tabs */ disallowMultipleTabs() { this.options.allowMultipleTabs = false; if (!this.isUsingFallback) { this.enforcer.options.allowMultipleTabs = false; } } /** * Force tab registration (useful for recovery) */ forceRegister() { if (!this.isUsingFallback) { this.enforcer.registerTab(); } } /** * Get supported features */ getSupportedFeatures() { return this.adapter.features; } /** * Get browser information */ getBrowserInfo() { return this.adapter.browserInfo; } /** * Destroy the tab enforcer */ destroy() { if (this.isUsingFallback) { this.fallbackStrategy.destroy(); } else { this.enforcer.destroy(); } } } // Static methods for convenience SoloTabEnforcer.create = function (options) { return new SoloTabEnforcer(options); }; SoloTabEnforcer.createAndInit = function (options) { const enforcer = new SoloTabEnforcer(options); enforcer.init(); return enforcer; }; // Feature detection utilities SoloTabEnforcer.checkSupport = function () { return CrossBrowserCompat.checkFeatureSupport(); }; SoloTabEnforcer.getBrowserInfo = function () { return CrossBrowserCompat.getBrowserInfo(); }; return SoloTabEnforcer; }));