detect-tab
Version:
A comprehensive tab detection and management library for web applications
526 lines (521 loc) • 16.7 kB
JavaScript
/**
* Tab visibility states
*/
var TabState;
(function (TabState) {
TabState["VISIBLE"] = "visible";
TabState["HIDDEN"] = "hidden";
TabState["PRERENDER"] = "prerender";
TabState["UNLOADED"] = "unloaded";
})(TabState || (TabState = {}));
/**
* Tab events that can be listened to
*/
var TabEvent;
(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";
})(TabEvent || (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(TabEvent.VISIBILITY_CHANGE);
}, this.options.debounceTime);
debouncedHandler();
};
this.handleFocus = () => {
const debouncedHandler = debounce(() => {
this.updateFocus(true);
this.emitEvent(TabEvent.FOCUS);
}, this.options.debounceTime);
debouncedHandler();
};
this.handleBlur = () => {
const debouncedHandler = debounce(() => {
this.updateFocus(false);
this.emitEvent(TabEvent.BLUR);
}, this.options.debounceTime);
debouncedHandler();
};
this.handleBeforeUnload = () => {
this.emitEvent(TabEvent.BEFORE_UNLOAD);
if (this.options.persistent) {
this.savePersistedData();
}
};
this.handleUnload = () => {
this.emitEvent(TabEvent.UNLOAD);
};
this.handlePageShow = () => {
this.emitEvent(TabEvent.PAGE_SHOW);
};
this.handlePageHide = () => {
this.emitEvent(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 === TabState.VISIBLE,
lastChanged: this.startTime,
timeInState: 0,
totalVisibleTime: 0,
totalHiddenTime: 0,
visibilityChanges: 0
};
}
getCurrentState() {
if (!isVisibilityAPISupported()) {
return TabState.VISIBLE;
}
switch (document.visibilityState) {
case 'visible':
return TabState.VISIBLE;
case 'hidden':
return TabState.HIDDEN;
default:
return document.visibilityState === 'prerender' ? TabState.PRERENDER : 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 === 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 : 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; };
export { DetectTab, TabEvent, TabState, debounce, DetectTab as default, formatDuration, getState, getTabInfo, getTimeStats, getVisibilityProperties, isBrowserSupported, isFocused, isStorageAvailable, isVisibilityAPISupported, isVisible, now, safeJSONParse };
//# sourceMappingURL=index.esm.js.map