UNPKG

detect-tab

Version:

A comprehensive tab detection and management library for web applications

550 lines (542 loc) 18.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DetectTab = {})); })(this, (function (exports) { 'use strict'; /** * Tab visibility states */ exports.TabState = void 0; (function (TabState) { TabState["VISIBLE"] = "visible"; TabState["HIDDEN"] = "hidden"; TabState["PRERENDER"] = "prerender"; TabState["UNLOADED"] = "unloaded"; })(exports.TabState || (exports.TabState = {})); /** * Tab events that can be listened to */ exports.TabEvent = void 0; (function (TabEvent) { TabEvent["VISIBILITY_CHANGE"] = "visibilitychange"; TabEvent["FOCUS"] = "focus"; TabEvent["BLUR"] = "blur"; TabEvent["BEFORE_UNLOAD"] = "beforeunload"; TabEvent["UNLOAD"] = "unload"; TabEvent["PAGE_SHOW"] = "pageshow"; TabEvent["PAGE_HIDE"] = "pagehide"; })(exports.TabEvent || (exports.TabEvent = {})); /** * Utility functions for tab detection */ /** * Check if the Page Visibility API is supported */ function isVisibilityAPISupported() { return typeof document !== 'undefined' && 'visibilityState' in document; } /** * Check if the browser is supported */ function isBrowserSupported() { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * Get the browser-specific visibility property names */ function getVisibilityProperties() { if (!isBrowserSupported()) { return { hidden: 'hidden', visibilityChange: 'visibilitychange' }; } let hidden; let visibilityChange; if (typeof document.hidden !== 'undefined') { hidden = 'hidden'; visibilityChange = 'visibilitychange'; } else if (typeof document.msHidden !== 'undefined') { hidden = 'msHidden'; visibilityChange = 'msvisibilitychange'; } else if (typeof document.webkitHidden !== 'undefined') { hidden = 'webkitHidden'; visibilityChange = 'webkitvisibilitychange'; } else { hidden = 'hidden'; visibilityChange = 'visibilitychange'; } return { hidden, visibilityChange }; } /** * Debounce function to limit rapid event firing */ function debounce(func, wait) { let timeout = null; return (...args) => { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func(...args); }, wait); }; } /** * Get current timestamp */ function now() { return Date.now(); } /** * Format time duration in human-readable format */ function formatDuration(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`; } else if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } /** * Safe JSON parse with fallback */ function safeJSONParse(json, fallback) { try { return JSON.parse(json) || fallback; } catch (_a) { return fallback; } } /** * Check if localStorage is available */ function isStorageAvailable() { try { if (typeof localStorage === 'undefined') return false; const test = '__storage_test__'; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch (_a) { return false; } } /** * Main DetectTab class for tab detection and management */ class DetectTab { constructor(options = {}) { this.listeners = new Map(); this.stateChangeListeners = new Set(); this.focusListeners = new Set(); this.isActive = false; this.handleVisibilityChange = () => { const debouncedHandler = debounce(() => { const newState = this.getCurrentState(); this.updateState(newState); this.emitEvent(exports.TabEvent.VISIBILITY_CHANGE); }, this.options.debounceTime); debouncedHandler(); }; this.handleFocus = () => { const debouncedHandler = debounce(() => { this.updateFocus(true); this.emitEvent(exports.TabEvent.FOCUS); }, this.options.debounceTime); debouncedHandler(); }; this.handleBlur = () => { const debouncedHandler = debounce(() => { this.updateFocus(false); this.emitEvent(exports.TabEvent.BLUR); }, this.options.debounceTime); debouncedHandler(); }; this.handleBeforeUnload = () => { this.emitEvent(exports.TabEvent.BEFORE_UNLOAD); if (this.options.persistent) { this.savePersistedData(); } }; this.handleUnload = () => { this.emitEvent(exports.TabEvent.UNLOAD); }; this.handlePageShow = () => { this.emitEvent(exports.TabEvent.PAGE_SHOW); }; this.handlePageHide = () => { this.emitEvent(exports.TabEvent.PAGE_HIDE); if (this.options.persistent) { this.savePersistedData(); } }; if (!isBrowserSupported()) { throw new Error('DetectTab requires a browser environment'); } this.options = { autoStart: true, debounceTime: 100, debug: false, storageKey: 'detectTab_data', persistent: false, ...options }; this.visibilityProperties = getVisibilityProperties(); this.startTime = now(); this.lastStateChange = this.startTime; // Initialize tab info this.info = this.createInitialTabInfo(); // Load persisted data if enabled if (this.options.persistent) { this.loadPersistedData(); } // Auto-start if enabled if (this.options.autoStart) { this.start(); } this.log('DetectTab initialized', this.info); } /** * Start tab detection */ start() { if (this.isActive) { this.log('Already active'); return; } this.isActive = true; this.attachEventListeners(); this.log('Tab detection started'); } /** * Stop tab detection */ stop() { if (!this.isActive) { this.log('Already inactive'); return; } this.isActive = false; this.detachEventListeners(); this.log('Tab detection stopped'); } /** * Get current tab information */ getTabInfo() { this.updateTimeInState(); return { ...this.info }; } /** * Check if tab is currently visible */ isVisible() { return this.info.visible; } /** * Check if tab is currently focused */ isFocused() { return this.info.focused; } /** * Get current tab state */ getState() { return this.info.state; } /** * Get formatted time statistics */ getTimeStats() { this.updateTimeInState(); return { totalVisibleTime: formatDuration(this.info.totalVisibleTime), totalHiddenTime: formatDuration(this.info.totalHiddenTime), timeInCurrentState: formatDuration(this.info.timeInState) }; } /** * Add event listener for specific tab events */ on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event).add(callback); this.log(`Added listener for ${event}`); } /** * Remove event listener */ off(event, callback) { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.delete(callback); if (eventListeners.size === 0) { this.listeners.delete(event); } this.log(`Removed listener for ${event}`); } } /** * Add state change listener */ onStateChange(callback) { this.stateChangeListeners.add(callback); this.log('Added state change listener'); } /** * Remove state change listener */ offStateChange(callback) { this.stateChangeListeners.delete(callback); this.log('Removed state change listener'); } /** * Add focus change listener */ onFocusChange(callback) { this.focusListeners.add(callback); this.log('Added focus change listener'); } /** * Remove focus change listener */ offFocusChange(callback) { this.focusListeners.delete(callback); this.log('Removed focus change listener'); } /** * Clear all persisted data */ clearPersistedData() { if (isStorageAvailable()) { localStorage.removeItem(this.options.storageKey); this.log('Cleared persisted data'); } } /** * Reset all statistics */ reset() { this.startTime = now(); this.lastStateChange = this.startTime; this.info = this.createInitialTabInfo(); this.log('Statistics reset'); } /** * Destroy the instance and clean up */ destroy() { this.stop(); this.listeners.clear(); this.stateChangeListeners.clear(); this.focusListeners.clear(); if (this.options.persistent) { this.savePersistedData(); } this.log('DetectTab destroyed'); } createInitialTabInfo() { const currentState = this.getCurrentState(); const isFocused = this.getCurrentFocus(); return { state: currentState, focused: isFocused, visible: currentState === exports.TabState.VISIBLE, lastChanged: this.startTime, timeInState: 0, totalVisibleTime: 0, totalHiddenTime: 0, visibilityChanges: 0 }; } getCurrentState() { if (!isVisibilityAPISupported()) { return exports.TabState.VISIBLE; } switch (document.visibilityState) { case 'visible': return exports.TabState.VISIBLE; case 'hidden': return exports.TabState.HIDDEN; default: return document.visibilityState === 'prerender' ? exports.TabState.PRERENDER : exports.TabState.VISIBLE; } } getCurrentFocus() { return document.hasFocus(); } attachEventListeners() { if (isVisibilityAPISupported()) { document.addEventListener(this.visibilityProperties.visibilityChange, this.handleVisibilityChange); } window.addEventListener('focus', this.handleFocus); window.addEventListener('blur', this.handleBlur); window.addEventListener('beforeunload', this.handleBeforeUnload); window.addEventListener('unload', this.handleUnload); window.addEventListener('pageshow', this.handlePageShow); window.addEventListener('pagehide', this.handlePageHide); } detachEventListeners() { if (isVisibilityAPISupported()) { document.removeEventListener(this.visibilityProperties.visibilityChange, this.handleVisibilityChange); } window.removeEventListener('focus', this.handleFocus); window.removeEventListener('blur', this.handleBlur); window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('unload', this.handleUnload); window.removeEventListener('pageshow', this.handlePageShow); window.removeEventListener('pagehide', this.handlePageHide); } updateState(newState) { if (newState === this.info.state) return; this.updateTimeInState(); const oldState = this.info.state; this.info.state = newState; this.info.visible = newState === exports.TabState.VISIBLE; this.info.lastChanged = now(); this.info.visibilityChanges++; this.lastStateChange = this.info.lastChanged; this.stateChangeListeners.forEach(callback => { try { callback(newState, this.getTabInfo()); } catch (error) { this.log('Error in state change callback:', error); } }); this.log(`State changed: ${oldState} -> ${newState}`); } updateFocus(focused) { if (focused === this.info.focused) return; this.info.focused = focused; this.info.lastChanged = now(); this.focusListeners.forEach(callback => { try { callback(focused, this.getTabInfo()); } catch (error) { this.log('Error in focus change callback:', error); } }); this.log(`Focus changed: ${focused}`); } updateTimeInState() { const currentTime = now(); const timeDiff = currentTime - this.lastStateChange; this.info.timeInState = timeDiff; if (this.info.visible) { this.info.totalVisibleTime += timeDiff; } else { this.info.totalHiddenTime += timeDiff; } this.lastStateChange = currentTime; } emitEvent(eventType) { const eventListeners = this.listeners.get(eventType); if (!eventListeners) return; const tabInfo = this.getTabInfo(); eventListeners.forEach(callback => { try { callback(tabInfo); } catch (error) { this.log(`Error in ${eventType} callback:`, error); } }); } savePersistedData() { if (!isStorageAvailable()) return; try { const data = { totalVisibleTime: this.info.totalVisibleTime, totalHiddenTime: this.info.totalHiddenTime, visibilityChanges: this.info.visibilityChanges, lastSaved: now() }; localStorage.setItem(this.options.storageKey, JSON.stringify(data)); this.log('Data persisted to localStorage'); } catch (error) { this.log('Failed to persist data:', error); } } loadPersistedData() { if (!isStorageAvailable()) return; try { const stored = localStorage.getItem(this.options.storageKey); if (stored) { const data = safeJSONParse(stored, { totalVisibleTime: 0, totalHiddenTime: 0, visibilityChanges: 0, lastSaved: 0 }); this.info.totalVisibleTime = data.totalVisibleTime || 0; this.info.totalHiddenTime = data.totalHiddenTime || 0; this.info.visibilityChanges = data.visibilityChanges || 0; this.log('Loaded persisted data:', data); } } catch (error) { this.log('Failed to load persisted data:', error); } } log(...args) { if (this.options.debug) { console.log('[DetectTab]', ...args); } } } // Main exports // Create a default instance for simple usage const defaultInstance = typeof window !== 'undefined' ? new DetectTab() : null; /** * Simple functions using the default instance */ const isVisible = () => { var _a; return (_a = defaultInstance === null || defaultInstance === void 0 ? void 0 : defaultInstance.isVisible()) !== null && _a !== void 0 ? _a : false; }; const isFocused = () => { var _a; return (_a = defaultInstance === null || defaultInstance === void 0 ? void 0 : defaultInstance.isFocused()) !== null && _a !== void 0 ? _a : false; }; const getState = () => { var _a; return (_a = defaultInstance === null || defaultInstance === void 0 ? void 0 : defaultInstance.getState()) !== null && _a !== void 0 ? _a : exports.TabState.VISIBLE; }; const getTabInfo = () => { var _a; return (_a = defaultInstance === null || defaultInstance === void 0 ? void 0 : defaultInstance.getTabInfo()) !== null && _a !== void 0 ? _a : null; }; const getTimeStats = () => { var _a; return (_a = defaultInstance === null || defaultInstance === void 0 ? void 0 : defaultInstance.getTimeStats()) !== null && _a !== void 0 ? _a : null; }; exports.DetectTab = DetectTab; exports.debounce = debounce; exports.default = DetectTab; exports.formatDuration = formatDuration; exports.getState = getState; exports.getTabInfo = getTabInfo; exports.getTimeStats = getTimeStats; exports.getVisibilityProperties = getVisibilityProperties; exports.isBrowserSupported = isBrowserSupported; exports.isFocused = isFocused; exports.isStorageAvailable = isStorageAvailable; exports.isVisibilityAPISupported = isVisibilityAPISupported; exports.isVisible = isVisible; exports.now = now; exports.safeJSONParse = safeJSONParse; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=index.umd.js.map