solo-tab-enforcer
Version:
Cross-browser solution for enforcing single tab usage in web applications
1 lines • 21.1 kB
JavaScript
(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';class CrossBrowserCompat{static isBrowser(){return typeof window !== 'undefined' && typeof document !== 'undefined';}static isNode(){return(typeof process !== 'undefined' && process.versions && process.versions.node);}static getBrowserInfo(){if(!this.isBrowser()){return{name: 'unknown',version: 'unknown'};}const ua = navigator.userAgent;let browserName = 'unknown';let browserVersion = 'unknown';if(ua.indexOf('Chrome')> -1){browserName = 'chrome';browserVersion = ua.match(/Chrome\/(\d+)/)?.[1] || 'unknown';}else if(ua.indexOf('Firefox')> -1){browserName = 'firefox';browserVersion = ua.match(/Firefox\/(\d+)/)?.[1] || 'unknown';}else if(ua.indexOf('Safari')> -1){browserName = 'safari';browserVersion = ua.match(/Version\/(\d+)/)?.[1] || 'unknown';}else if(ua.indexOf('Edge')> -1){browserName = 'edge';browserVersion = ua.match(/Edge\/(\d+)/)?.[1] || 'unknown';}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};}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',};}static applyPolyfills(){if(!this.isBrowser()){return;}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;};}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;};}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;}};}}static getStorageImplementation(){if(!this.isBrowser()){return null;}const support = this.checkFeatureSupport();if(support.localStorage){return localStorage;}else if(support.sessionStorage){return sessionStorage;}else{return this.getCookieStorage();}}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=/';},};}static generateUniqueId(){if(this.isBrowser()&& typeof crypto !== 'undefined' && crypto.getRandomValues){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);}}static addEventListener(element,event,handler,options){if(!this.isBrowser()){return;}if(element.addEventListener){element.addEventListener(event,handler,options);}else if(element.attachEvent){element.attachEvent('on' + event,handler);}else{element['on' + event] = handler;}}static removeEventListener(element,event,handler,options){if(!this.isBrowser()){return;}if(element.removeEventListener){element.removeEventListener(event,handler,options);}else if(element.detachEvent){element.detachEvent('on' + event,handler);}else{element['on' + event] = null;}}}class BrowserAdapter{constructor(){this.browserInfo = this.getBrowserInfo();this.features = this.detectFeatures();}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'};}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',};}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{return{type: 'cookies',get:(key)=> this.getCookie(key),set:(key,value)=> this.setCookie(key,value),remove:(key)=> this.removeCookie(key),};}}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{return{type: 'polling',create:(key)=>({key}),send:()=>{},close:()=>{},};}}getVisibilityMethod(){if(this.features.visibilityAPI){return{type: 'visibilityAPI',isVisible:()=> document.visibilityState === 'visible',onVisibilityChange:(callback)=>{document.addEventListener('visibilitychange',callback);return()=> document.removeEventListener('visibilitychange',callback);},};}else{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);};},};}}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=/`;}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);}}getOptimizations(){const optimizations ={checkInterval: 1000,heartbeatInterval: 5000,timeoutMs: 10000,};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;}}class FallbackStrategy{constructor(options ={}){this.options ={pollInterval: 2000,maxRetries: 3,retryDelay: 1000,...options,};this.isActive = false;this.pollTimer = null;this.retryCount = 0;}init(){this.startPolling();}startPolling(){this.pollTimer = setInterval(()=>{this.checkForConflicts();},this.options.pollInterval);}checkForConflicts(){try{const originalTitle = document.title;const marker = '__tab_check__';document.title = marker;if(document.title === marker){document.title = originalTitle;this.handleTabActivated();}else{this.handleTabConflict();}}catch(error){this.handleError(error);}}useHashStrategy(){const tabId = this.generateTabId();const originalHash = window.location.hash;window.location.hash = `#tab_${tabId}`;setTimeout(()=>{if(window.location.hash === `#tab_${tabId}`){this.handleTabActivated();}else{this.handleTabConflict();}window.location.hash = originalHash;},100);}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();}}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){window.__tabEnforcer ={tabId: tabId,timestamp: Date.now(),};this.handleTabActivated();}else if(window.__tabEnforcer.tabId !== tabId){this.handleTabConflict();}}}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){document.body.removeChild(iframe);this.handleError(error);}}handleTabActivated(){this.isActive = true;this.retryCount = 0;if(this.options.onTabActivated){this.options.onTabActivated();}}handleTabConflict(){this.isActive = false;if(this.options.onTabConflict){this.options.onTabConflict();}}handleError(error){console.warn('Fallback strategy error:',error);if(this.retryCount < this.options.maxRetries){this.retryCount++;setTimeout(()=>{this.checkForConflicts();},this.options.retryDelay);}}generateTabId(){return Date.now().toString(36)+ Math.random().toString(36).substr(2,5);}destroy(){if(this.pollTimer){clearInterval(this.pollTimer);}}}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);}generateTabId(){return `tab_${Date.now()}_${Math.random().toString(36).substr(2,9)}`;}detectFeatures(){return{broadcastChannel: typeof BroadcastChannel !== 'undefined',visibilityAPI: typeof document.visibilityState !== 'undefined',localStorage: typeof localStorage !== 'undefined',sessionStorage: typeof sessionStorage !== 'undefined',storageEvents: typeof window.addEventListener !== 'undefined',};}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');}setupEventListeners(){if(this.options.useBroadcastChannel && this.supportedFeatures.broadcastChannel){this.setupBroadcastChannel();}if(this.options.useStorageEvents && this.supportedFeatures.storageEvents){this.setupStorageEvents();}if(this.options.useVisibilityAPI && this.supportedFeatures.visibilityAPI){this.setupVisibilityAPI();}this.setupFocusEvents();this.setupUnloadEvents();}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);}}setupStorageEvents(){this.storageEventListener =(event)=>{if(event.key === this.options.storageKey){this.handleStorageEvent(event);}};window.addEventListener('storage',this.storageEventListener);this.log('Storage events initialized');}setupVisibilityAPI(){this.visibilityChangeListener =()=>{if(document.visibilityState === 'visible'){this.handleTabActivated();}else{this.handleTabDeactivated();}};document.addEventListener('visibilitychange',this.visibilityChangeListener);this.log('Visibility API initialized');}setupFocusEvents(){this.focusListener =()=> this.handleTabActivated();this.blurListener =()=> this.handleTabDeactivated();window.addEventListener('focus',this.focusListener);window.addEventListener('blur',this.blurListener);this.log('Focus events initialized');}setupUnloadEvents(){this.beforeUnloadListener =()=>{this.unregisterTab();};window.addEventListener('beforeunload',this.beforeUnloadListener);window.addEventListener('unload',this.beforeUnloadListener);this.log('Unload events initialized');}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);}unregisterTab(){this.removeStorageData();this.broadcastMessage({type: 'tab-unregistered',tabId: this.tabId});this.log('Tab unregistered:',this.tabId);}startHeartbeat(){this.heartbeatTimer = setInterval(()=>{this.updateHeartbeat();},this.options.checkInterval);}updateHeartbeat(){const existingData = this.getStorageData();if(existingData && existingData.id === this.tabId){existingData.timestamp = Date.now();this.setStorageData(existingData);}}startTabCheck(){this.checkTimer = setInterval(()=>{this.checkForConflicts();},this.options.checkInterval);}checkForConflicts(){const existingData = this.getStorageData();if(!existingData){this.registerTab();return;}const timeDiff = Date.now()- existingData.timestamp;if(timeDiff > this.options.tabTimeoutMs){this.registerTab();return;}if(existingData.id !== this.tabId){this.handleTabConflict(existingData);}}handleTabConflict(existingTab){this.log('Tab conflict detected:',existingTab);if(this.options.onTabConflict){this.options.onTabConflict(existingTab);}else{this.showDefaultWarning();}}showDefaultWarning(){if(this.options.redirectUrl){window.location.href = this.options.redirectUrl;}else{alert(this.options.warningMessage);window.close();}}handleTabActivated(){this.isActive = true;this.log('Tab activated');if(this.options.onTabActivated){this.options.onTabActivated();}this.registerTab();}handleTabDeactivated(){this.isActive = false;this.log('Tab deactivated');if(this.options.onTabDeactivated){this.options.onTabDeactivated();}}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': if(data.tabId !== this.tabId){setTimeout(()=> this.registerTab(),100);}break;}}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();}}}broadcastMessage(data){if(this.broadcastChannel){try{this.broadcastChannel.postMessage(data);}catch(error){this.log('Failed to broadcast message:',error);}}}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;}}setStorageData(data){try{localStorage.setItem(this.options.storageKey,JSON.stringify(data));}catch(error){this.log('Failed to set storage data:',error);}}removeStorageData(){try{localStorage.removeItem(this.options.storageKey);}catch(error){this.log('Failed to remove storage data:',error);}}getTabInfo(){return{id: this.tabId,isActive: this.isActive,isInitialized: this.isInitialized,supportedFeatures: this.supportedFeatures,options: this.options,};}destroy(){this.log('Destroying TabEnforcer...');if(this.checkTimer){clearInterval(this.checkTimer);}if(this.heartbeatTimer){clearInterval(this.heartbeatTimer);}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);}if(this.broadcastChannel){this.broadcastChannel.close();}this.unregisterTab();this.isInitialized = false;this.log('TabEnforcer destroyed');}log(...args){if(this.options.debug){console.log(`[TabEnforcer:${this.tabId}]`,...args);}}}class SoloTabEnforcer{constructor(options ={}){this.adapter = new BrowserAdapter();this.options ={...this.adapter.getOptimizations(),...options,};this.enforcer = new TabEnforcer(this.options);this.fallbackStrategy = new FallbackStrategy({...this.options,onTabActivated:()=> this.handleTabActivated(),onTabConflict:()=> this.handleTabConflict(),});this.isUsingFallback = false;}init(){if(this.adapter.features.localStorage && this.adapter.features.storageEvents){this.enforcer.init();}else{this.isUsingFallback = true;this.fallbackStrategy.init();}}handleTabActivated(){if(this.options.onTabActivated){this.options.onTabActivated();}}handleTabConflict(){if(this.options.onTabConflict){this.options.onTabConflict();}}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(),};}}areMultipleTabsAllowed(){return this.options.allowMultipleTabs;}allowMultipleTabs(){this.options.allowMultipleTabs = true;if(!this.isUsingFallback){this.enforcer.options.allowMultipleTabs = true;}}disallowMultipleTabs(){this.options.allowMultipleTabs = false;if(!this.isUsingFallback){this.enforcer.options.allowMultipleTabs = false;}}forceRegister(){if(!this.isUsingFallback){this.enforcer.registerTab();}}getSupportedFeatures(){return this.adapter.features;}getBrowserInfo(){return this.adapter.browserInfo;}destroy(){if(this.isUsingFallback){this.fallbackStrategy.destroy();}else{this.enforcer.destroy();}}}SoloTabEnforcer.create = function(options){return new SoloTabEnforcer(options);};SoloTabEnforcer.createAndInit = function(options){const enforcer = new SoloTabEnforcer(options);enforcer.init();return enforcer;};SoloTabEnforcer.checkSupport = function(){return CrossBrowserCompat.checkFeatureSupport();};SoloTabEnforcer.getBrowserInfo = function(){return CrossBrowserCompat.getBrowserInfo();};return SoloTabEnforcer;}));