solo-tab-enforcer
Version:
Cross-browser solution for enforcing single tab usage in web applications
1,401 lines (1,228 loc) • 35 kB
JavaScript
/**
* 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;
}));