@sc4rfurryx/proteusjs
Version:
The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.
1,578 lines (1,567 loc) • 603 kB
JavaScript
/*!
* ProteusJS v2.0.0
* Shape-shifting responsive design that adapts like the sea god himself
* (c) 2025 sc4rfurry
* Released under the MIT License
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* ProteusJS Logger
* Centralized logging system with configurable levels and production-safe output
*/
class Logger {
constructor(config = {}) {
this.levels = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4
};
this.config = {
level: 'warn',
prefix: 'ProteusJS',
enableInProduction: false,
enableTimestamps: false,
enableStackTrace: false,
...config
};
}
static getInstance(config) {
if (!Logger.instance) {
Logger.instance = new Logger(config);
}
return Logger.instance;
}
static configure(config) {
if (Logger.instance) {
Logger.instance.config = { ...Logger.instance.config, ...config };
}
else {
Logger.instance = new Logger(config);
}
}
shouldLog(level) {
// Don't log in production unless explicitly enabled
if (process.env['NODE_ENV'] === 'production' && !this.config.enableInProduction) {
return level === 'error'; // Only errors in production
}
return this.levels[level] >= this.levels[this.config.level];
}
formatMessage(level, message, ...args) {
let formattedMessage = message;
// Add prefix
if (this.config.prefix) {
formattedMessage = `${this.config.prefix}: ${formattedMessage}`;
}
// Add timestamp
if (this.config.enableTimestamps) {
const timestamp = new Date().toISOString();
formattedMessage = `[${timestamp}] ${formattedMessage}`;
}
// Add log level
const levelPrefix = level.toUpperCase().padEnd(5);
formattedMessage = `[${levelPrefix}] ${formattedMessage}`;
return [formattedMessage, ...args];
}
debug(message, ...args) {
if (!this.shouldLog('debug'))
return;
const [formattedMessage, ...formattedArgs] = this.formatMessage('debug', message, ...args);
console.debug(formattedMessage, ...formattedArgs);
if (this.config.enableStackTrace) {
console.trace();
}
}
info(message, ...args) {
if (!this.shouldLog('info'))
return;
const [formattedMessage, ...formattedArgs] = this.formatMessage('info', message, ...args);
console.info(formattedMessage, ...formattedArgs);
}
warn(message, ...args) {
if (!this.shouldLog('warn'))
return;
const [formattedMessage, ...formattedArgs] = this.formatMessage('warn', message, ...args);
console.warn(formattedMessage, ...formattedArgs);
}
error(message, error, ...args) {
if (!this.shouldLog('error'))
return;
const [formattedMessage, ...formattedArgs] = this.formatMessage('error', message, ...args);
if (error instanceof Error) {
console.error(formattedMessage, error, ...formattedArgs);
if (this.config.enableStackTrace && error.stack) {
console.error('Stack trace:', error.stack);
}
}
else if (error) {
console.error(formattedMessage, error, ...formattedArgs);
}
else {
console.error(formattedMessage, ...formattedArgs);
}
}
group(label) {
if (!this.shouldLog('info'))
return;
console.group(`${this.config.prefix}: ${label}`);
}
groupEnd() {
if (!this.shouldLog('info'))
return;
console.groupEnd();
}
time(label) {
if (!this.shouldLog('debug'))
return;
console.time(`${this.config.prefix}: ${label}`);
}
timeEnd(label) {
if (!this.shouldLog('debug'))
return;
console.timeEnd(`${this.config.prefix}: ${label}`);
}
setLevel(level) {
this.config.level = level;
}
getLevel() {
return this.config.level;
}
isEnabled(level) {
return this.shouldLog(level);
}
}
// Create default logger instance
const logger = Logger.getInstance({
level: process.env['NODE_ENV'] === 'development' ? 'debug' : 'warn',
prefix: 'ProteusJS',
enableInProduction: false,
enableTimestamps: process.env['NODE_ENV'] === 'development',
enableStackTrace: false
});
/**
* Event System for ProteusJS
* Handles all internal and external events
*/
class EventSystem {
constructor() {
this.listeners = new Map();
this.initialized = false;
}
/**
* Initialize the event system
*/
init() {
if (this.initialized)
return;
this.initialized = true;
}
/**
* Add event listener
*/
on(eventType, callback) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, new Set());
}
const callbacks = this.listeners.get(eventType);
callbacks.add(callback);
// Return unsubscribe function
return () => {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(eventType);
}
};
}
/**
* Add one-time event listener
*/
once(eventType, callback) {
const unsubscribe = this.on(eventType, (event) => {
callback(event);
unsubscribe();
});
return unsubscribe;
}
/**
* Remove event listener
*/
off(eventType, callback) {
if (!callback) {
// Remove all listeners for this event type
this.listeners.delete(eventType);
return;
}
const callbacks = this.listeners.get(eventType);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(eventType);
}
}
}
/**
* Emit event
*/
emit(eventType, detail, target) {
const callbacks = this.listeners.get(eventType);
if (!callbacks || callbacks.size === 0)
return;
const event = {
type: eventType,
target: target || document.documentElement,
detail,
timestamp: Date.now()
};
// Execute callbacks
callbacks.forEach(callback => {
try {
callback(event);
}
catch (error) {
console.error(`ProteusJS: Error in event listener for "${eventType}":`, error);
}
});
}
/**
* Get all event types with listeners
*/
getEventTypes() {
return Array.from(this.listeners.keys());
}
/**
* Get listener count for event type
*/
getListenerCount(eventType) {
const callbacks = this.listeners.get(eventType);
return callbacks ? callbacks.size : 0;
}
/**
* Check if event type has listeners
*/
hasListeners(eventType) {
return this.getListenerCount(eventType) > 0;
}
/**
* Clear all listeners
*/
clear() {
this.listeners.clear();
}
/**
* Destroy the event system
*/
destroy() {
this.clear();
this.initialized = false;
}
/**
* Get debug information
*/
getDebugInfo() {
const info = {};
this.listeners.forEach((callbacks, eventType) => {
info[eventType] = callbacks.size;
});
return {
initialized: this.initialized,
totalEventTypes: this.listeners.size,
listeners: info
};
}
}
/**
* Plugin System for ProteusJS
* Enables extensibility and modular architecture
*/
class PluginSystem {
constructor(proteus) {
this.plugins = new Map();
this.installedPlugins = new Set();
this.initialized = false;
this.proteus = proteus;
}
/**
* Initialize the plugin system
*/
init() {
if (this.initialized)
return;
this.initialized = true;
}
/**
* Register a plugin
*/
register(plugin) {
if (this.plugins.has(plugin.name)) {
console.warn(`ProteusJS: Plugin "${plugin.name}" is already registered`);
return this;
}
// Validate plugin
if (!this.validatePlugin(plugin)) {
throw new Error(`ProteusJS: Invalid plugin "${plugin.name}"`);
}
this.plugins.set(plugin.name, plugin);
return this;
}
/**
* Install a plugin
*/
install(pluginName) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`ProteusJS: Plugin "${pluginName}" not found`);
}
if (this.installedPlugins.has(pluginName)) {
console.warn(`ProteusJS: Plugin "${pluginName}" is already installed`);
return this;
}
// Check dependencies
if (plugin.dependencies) {
for (const dep of plugin.dependencies) {
if (!this.installedPlugins.has(dep)) {
throw new Error(`ProteusJS: Plugin "${pluginName}" requires dependency "${dep}" to be installed first`);
}
}
}
try {
// Install the plugin
plugin.install(this.proteus);
this.installedPlugins.add(pluginName);
// Emit plugin installed event
this.proteus.getEventSystem().emit('pluginInstalled', {
plugin: pluginName,
version: plugin.version
});
console.log(`ProteusJS: Plugin "${pluginName}" v${plugin.version} installed`);
}
catch (error) {
console.error(`ProteusJS: Failed to install plugin "${pluginName}":`, error);
throw error;
}
return this;
}
/**
* Uninstall a plugin
*/
uninstall(pluginName) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
console.warn(`ProteusJS: Plugin "${pluginName}" not found`);
return this;
}
if (!this.installedPlugins.has(pluginName)) {
console.warn(`ProteusJS: Plugin "${pluginName}" is not installed`);
return this;
}
// Check if other plugins depend on this one
const dependents = this.getDependents(pluginName);
if (dependents.length > 0) {
throw new Error(`ProteusJS: Cannot uninstall plugin "${pluginName}" because it's required by: ${dependents.join(', ')}`);
}
try {
// Uninstall the plugin
if (plugin.uninstall) {
plugin.uninstall(this.proteus);
}
this.installedPlugins.delete(pluginName);
// Emit plugin uninstalled event
this.proteus.getEventSystem().emit('pluginUninstalled', {
plugin: pluginName,
version: plugin.version
});
console.log(`ProteusJS: Plugin "${pluginName}" uninstalled`);
}
catch (error) {
console.error(`ProteusJS: Failed to uninstall plugin "${pluginName}":`, error);
throw error;
}
return this;
}
/**
* Check if a plugin is installed
*/
isInstalled(pluginName) {
return this.installedPlugins.has(pluginName);
}
/**
* Get list of registered plugins
*/
getRegisteredPlugins() {
return Array.from(this.plugins.keys());
}
/**
* Get list of installed plugins
*/
getInstalledPlugins() {
return Array.from(this.installedPlugins);
}
/**
* Get plugin information
*/
getPluginInfo(pluginName) {
return this.plugins.get(pluginName);
}
/**
* Install multiple plugins in dependency order
*/
installMany(pluginNames) {
const sortedPlugins = this.sortByDependencies(pluginNames);
for (const pluginName of sortedPlugins) {
this.install(pluginName);
}
return this;
}
/**
* Destroy the plugin system
*/
destroy() {
// Uninstall all plugins in reverse dependency order
const installedPlugins = Array.from(this.installedPlugins);
const sortedPlugins = this.sortByDependencies(installedPlugins).reverse();
for (const pluginName of sortedPlugins) {
try {
this.uninstall(pluginName);
}
catch (error) {
console.error(`ProteusJS: Error uninstalling plugin "${pluginName}":`, error);
}
}
this.plugins.clear();
this.installedPlugins.clear();
this.initialized = false;
}
/**
* Validate plugin structure
*/
validatePlugin(plugin) {
if (!plugin.name || typeof plugin.name !== 'string') {
console.error('ProteusJS: Plugin must have a valid name');
return false;
}
if (!plugin.version || typeof plugin.version !== 'string') {
console.error('ProteusJS: Plugin must have a valid version');
return false;
}
if (!plugin.install || typeof plugin.install !== 'function') {
console.error('ProteusJS: Plugin must have an install function');
return false;
}
return true;
}
/**
* Get plugins that depend on the given plugin
*/
getDependents(pluginName) {
const dependents = [];
for (const [name, plugin] of this.plugins) {
if (this.installedPlugins.has(name) && plugin.dependencies?.includes(pluginName)) {
dependents.push(name);
}
}
return dependents;
}
/**
* Sort plugins by dependency order
*/
sortByDependencies(pluginNames) {
const sorted = [];
const visited = new Set();
const visiting = new Set();
const visit = (pluginName) => {
if (visiting.has(pluginName)) {
throw new Error(`ProteusJS: Circular dependency detected involving plugin "${pluginName}"`);
}
if (visited.has(pluginName))
return;
visiting.add(pluginName);
const plugin = this.plugins.get(pluginName);
if (plugin?.dependencies) {
for (const dep of plugin.dependencies) {
if (pluginNames.includes(dep)) {
visit(dep);
}
}
}
visiting.delete(pluginName);
visited.add(pluginName);
sorted.push(pluginName);
};
for (const pluginName of pluginNames) {
visit(pluginName);
}
return sorted;
}
}
/**
* Memory Manager for ProteusJS
* Prevents memory leaks and manages resource cleanup
*/
class MemoryManager {
constructor() {
this.resources = new Map();
this.elementResources = new Map();
this.mutationObserver = null;
this.cleanupInterval = null;
this.isMonitoring = false;
this.setupDOMObserver();
this.startPeriodicCleanup();
}
/**
* Register a resource for automatic cleanup
*/
register(resource) {
const fullResource = {
...resource,
timestamp: Date.now()
};
this.resources.set(resource.id, fullResource);
// Track element-specific resources
if (resource.element) {
if (!this.elementResources.has(resource.element)) {
this.elementResources.set(resource.element, new Set());
}
this.elementResources.get(resource.element).add(resource.id);
}
return resource.id;
}
/**
* Unregister and cleanup a resource
*/
unregister(resourceId) {
const resource = this.resources.get(resourceId);
if (!resource)
return false;
try {
resource.cleanup();
}
catch (error) {
console.error(`ProteusJS: Error cleaning up resource ${resourceId}:`, error);
}
this.resources.delete(resourceId);
// Remove from element tracking
if (resource.element) {
const elementResources = this.elementResources.get(resource.element);
if (elementResources) {
elementResources.delete(resourceId);
if (elementResources.size === 0) {
this.elementResources.delete(resource.element);
}
}
}
return true;
}
/**
* Cleanup all resources associated with an element
*/
cleanupElement(element) {
const resourceIds = this.elementResources.get(element);
if (!resourceIds)
return 0;
let cleanedCount = 0;
resourceIds.forEach(resourceId => {
if (this.unregister(resourceId)) {
cleanedCount++;
}
});
return cleanedCount;
}
/**
* Cleanup resources by type
*/
cleanupByType(type) {
let cleanedCount = 0;
const toCleanup = [];
this.resources.forEach((resource, id) => {
if (resource.type === type) {
toCleanup.push(id);
}
});
toCleanup.forEach(id => {
if (this.unregister(id)) {
cleanedCount++;
}
});
return cleanedCount;
}
/**
* Cleanup old resources based on age
*/
cleanupOldResources(maxAge = 300000) {
let cleanedCount = 0;
const now = Date.now();
const toCleanup = [];
this.resources.forEach((resource, id) => {
if (now - resource.timestamp > maxAge) {
toCleanup.push(id);
}
});
toCleanup.forEach(id => {
if (this.unregister(id)) {
cleanedCount++;
}
});
return cleanedCount;
}
/**
* Force garbage collection if available
*/
forceGarbageCollection() {
if ('gc' in window && typeof window.gc === 'function') {
try {
window.gc();
}
catch (error) {
// Ignore errors - gc might not be available
}
}
}
/**
* Get memory usage information
*/
getMemoryInfo() {
const info = {
managedResources: this.resources.size,
trackedElements: this.elementResources.size,
resourcesByType: {}
};
// Count resources by type
this.resources.forEach(resource => {
info.resourcesByType[resource.type] = (info.resourcesByType[resource.type] || 0) + 1;
});
// Add browser memory info if available
if ('memory' in performance) {
const memory = performance.memory;
info.browserMemory = {
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit
};
}
return info;
}
/**
* Check for potential memory leaks
*/
detectLeaks() {
const warnings = [];
const now = Date.now();
// Check for too many resources
if (this.resources.size > 1000) {
warnings.push(`High number of managed resources: ${this.resources.size}`);
}
// Check for old resources
let oldResourceCount = 0;
this.resources.forEach(resource => {
if (now - resource.timestamp > 600000) { // 10 minutes
oldResourceCount++;
}
});
if (oldResourceCount > 50) {
warnings.push(`Many old resources detected: ${oldResourceCount}`);
}
// Check for orphaned element resources
let orphanedCount = 0;
this.elementResources.forEach((resourceIds, element) => {
if (!document.contains(element)) {
orphanedCount += resourceIds.size;
}
});
if (orphanedCount > 0) {
warnings.push(`Orphaned element resources detected: ${orphanedCount}`);
}
return warnings;
}
/**
* Cleanup all resources and stop monitoring
*/
destroy() {
// Cleanup all resources
const resourceIds = Array.from(this.resources.keys());
resourceIds.forEach(id => this.unregister(id));
// Stop monitoring
this.stopMonitoring();
// Clear maps
this.resources.clear();
this.elementResources.clear();
}
/**
* Start monitoring for memory leaks
*/
startMonitoring() {
if (this.isMonitoring)
return;
this.isMonitoring = true;
}
/**
* Stop monitoring
*/
stopMonitoring() {
if (!this.isMonitoring)
return;
this.isMonitoring = false;
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
/**
* Setup DOM mutation observer to detect removed elements
*/
setupDOMObserver() {
if (typeof MutationObserver === 'undefined')
return;
this.mutationObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.handleElementRemoval(node);
}
});
}
});
});
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Handle element removal from DOM
*/
handleElementRemoval(element) {
// Cleanup resources for this element
this.cleanupElement(element);
// Also check descendants
const descendants = element.querySelectorAll('*');
descendants.forEach(descendant => {
this.cleanupElement(descendant);
});
}
/**
* Start periodic cleanup
*/
startPeriodicCleanup() {
this.cleanupInterval = window.setInterval(() => {
// Cleanup orphaned resources
let orphanedCount = 0;
this.elementResources.forEach((resourceIds, element) => {
if (!document.contains(element)) {
orphanedCount += this.cleanupElement(element);
}
});
// Cleanup very old resources
const oldCount = this.cleanupOldResources(600000); // 10 minutes
if (orphanedCount > 0 || oldCount > 0) {
console.log(`ProteusJS: Cleaned up ${orphanedCount} orphaned and ${oldCount} old resources`);
}
}, 60000); // Run every minute
}
}
/**
* ResizeObserver Polyfill for ProteusJS
* Provides ResizeObserver functionality for browsers that don't support it
*/
class ResizeObserverPolyfill {
constructor(callback) {
this.observedElements = new Map();
this.rafId = null;
this.isObserving = false;
this.callback = callback;
}
/**
* Start observing an element for resize changes
*/
observe(element, options) {
if (this.observedElements.has(element))
return;
const rect = element.getBoundingClientRect();
this.observedElements.set(element, {
lastWidth: rect.width,
lastHeight: rect.height
});
if (!this.isObserving) {
this.startObserving();
}
}
/**
* Stop observing an element
*/
unobserve(element) {
this.observedElements.delete(element);
if (this.observedElements.size === 0) {
this.stopObserving();
}
}
/**
* Disconnect all observations
*/
disconnect() {
this.observedElements.clear();
this.stopObserving();
}
/**
* Start the polling mechanism
*/
startObserving() {
if (this.isObserving)
return;
this.isObserving = true;
this.checkForChanges();
}
/**
* Stop the polling mechanism
*/
stopObserving() {
if (!this.isObserving)
return;
this.isObserving = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
/**
* Check for size changes in observed elements
*/
checkForChanges() {
if (!this.isObserving)
return;
const changedEntries = [];
this.observedElements.forEach((lastSize, element) => {
// Check if element is still in DOM
if (!document.contains(element)) {
this.observedElements.delete(element);
return;
}
const rect = element.getBoundingClientRect();
const currentWidth = rect.width;
const currentHeight = rect.height;
if (currentWidth !== lastSize.lastWidth || currentHeight !== lastSize.lastHeight) {
// Update stored size
this.observedElements.set(element, {
lastWidth: currentWidth,
lastHeight: currentHeight
});
// Create entry
const entry = {
target: element,
contentRect: this.createDOMRectReadOnly(rect),
contentBoxSize: [{
inlineSize: currentWidth,
blockSize: currentHeight
}],
borderBoxSize: [{
inlineSize: currentWidth,
blockSize: currentHeight
}]
};
changedEntries.push(entry);
}
});
// Call callback if there are changes
if (changedEntries.length > 0) {
try {
this.callback(changedEntries);
}
catch (error) {
console.error('ResizeObserver callback error:', error);
}
}
// Schedule next check
if (this.isObserving) {
this.rafId = requestAnimationFrame(() => this.checkForChanges());
}
}
/**
* Create a DOMRectReadOnly-like object
*/
createDOMRectReadOnly(rect) {
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
toJSON: () => ({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left
})
};
}
}
// Add static method for feature detection
ResizeObserverPolyfill.isSupported = () => {
return typeof ResizeObserver !== 'undefined';
};
/**
* IntersectionObserver Polyfill for ProteusJS
* Provides IntersectionObserver functionality for browsers that don't support it
*/
class IntersectionObserverPolyfill {
constructor(callback, options) {
this.observedElements = new Map();
this.rafId = null;
this.isObserving = false;
this.callback = callback;
this.root = (options?.root instanceof Element ? options.root : null);
this.rootMargin = options?.rootMargin || '0px';
this.thresholds = this.normalizeThresholds(options?.threshold);
this.parsedRootMargin = this.parseRootMargin(this.rootMargin);
}
/**
* Start observing an element
*/
observe(element) {
if (this.observedElements.has(element))
return;
this.observedElements.set(element, {
lastRatio: 0,
wasIntersecting: false
});
if (!this.isObserving) {
this.startObserving();
}
}
/**
* Stop observing an element
*/
unobserve(element) {
this.observedElements.delete(element);
if (this.observedElements.size === 0) {
this.stopObserving();
}
}
/**
* Disconnect all observations
*/
disconnect() {
this.observedElements.clear();
this.stopObserving();
}
/**
* Start the polling mechanism
*/
startObserving() {
if (this.isObserving)
return;
this.isObserving = true;
this.checkForIntersections();
}
/**
* Stop the polling mechanism
*/
stopObserving() {
if (!this.isObserving)
return;
this.isObserving = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
/**
* Check for intersection changes
*/
checkForIntersections() {
if (!this.isObserving)
return;
const changedEntries = [];
const rootBounds = this.getRootBounds();
this.observedElements.forEach((lastState, element) => {
// Check if element is still in DOM
if (!document.contains(element)) {
this.observedElements.delete(element);
return;
}
const targetRect = element.getBoundingClientRect();
const intersectionRect = this.calculateIntersection(targetRect, rootBounds);
const intersectionRatio = this.calculateIntersectionRatio(targetRect, intersectionRect);
const isIntersecting = intersectionRatio > 0;
// Check if we should trigger callback
const shouldTrigger = this.shouldTriggerCallback(intersectionRatio, lastState.lastRatio, isIntersecting, lastState.wasIntersecting);
if (shouldTrigger) {
// Update stored state
this.observedElements.set(element, {
lastRatio: intersectionRatio,
wasIntersecting: isIntersecting
});
// Create entry
const entry = {
target: element,
boundingClientRect: this.createDOMRectReadOnly(targetRect),
intersectionRect: this.createDOMRectReadOnly(intersectionRect),
rootBounds: rootBounds ? this.createDOMRectReadOnly(rootBounds) : null,
intersectionRatio,
isIntersecting,
time: performance.now()
};
changedEntries.push(entry);
}
});
// Call callback if there are changes
if (changedEntries.length > 0) {
try {
this.callback(changedEntries);
}
catch (error) {
console.error('IntersectionObserver callback error:', error);
}
}
// Schedule next check
if (this.isObserving) {
this.rafId = requestAnimationFrame(() => this.checkForIntersections());
}
}
/**
* Get root bounds with margin applied
*/
getRootBounds() {
const rootElement = this.root || document.documentElement;
const rect = rootElement.getBoundingClientRect();
return new DOMRect(rect.left - this.parsedRootMargin.left, rect.top - this.parsedRootMargin.top, rect.width + this.parsedRootMargin.left + this.parsedRootMargin.right, rect.height + this.parsedRootMargin.top + this.parsedRootMargin.bottom);
}
/**
* Calculate intersection rectangle
*/
calculateIntersection(targetRect, rootBounds) {
const left = Math.max(targetRect.left, rootBounds.left);
const top = Math.max(targetRect.top, rootBounds.top);
const right = Math.min(targetRect.right, rootBounds.right);
const bottom = Math.min(targetRect.bottom, rootBounds.bottom);
const width = Math.max(0, right - left);
const height = Math.max(0, bottom - top);
return new DOMRect(left, top, width, height);
}
/**
* Calculate intersection ratio
*/
calculateIntersectionRatio(targetRect, intersectionRect) {
const targetArea = targetRect.width * targetRect.height;
if (targetArea === 0)
return 0;
const intersectionArea = intersectionRect.width * intersectionRect.height;
return intersectionArea / targetArea;
}
/**
* Check if callback should be triggered based on thresholds
*/
shouldTriggerCallback(currentRatio, lastRatio, isIntersecting, wasIntersecting) {
// Always trigger on first observation
if (lastRatio === 0 && !wasIntersecting) {
return true;
}
// Check if intersection state changed
if (isIntersecting !== wasIntersecting) {
return true;
}
// Check if any threshold was crossed
for (const threshold of this.thresholds) {
if ((lastRatio < threshold && currentRatio >= threshold) ||
(lastRatio > threshold && currentRatio <= threshold)) {
return true;
}
}
return false;
}
/**
* Normalize threshold values
*/
normalizeThresholds(threshold) {
if (threshold === undefined)
return [0];
if (typeof threshold === 'number')
return [threshold];
return threshold.slice().sort((a, b) => a - b);
}
/**
* Parse root margin string
*/
parseRootMargin(margin) {
const values = margin.split(/\s+/).map(value => {
const num = parseFloat(value);
return value.endsWith('%') ? (num / 100) * window.innerHeight : num;
});
switch (values.length) {
case 1: return { top: values[0], right: values[0], bottom: values[0], left: values[0] };
case 2: return { top: values[0], right: values[1], bottom: values[0], left: values[1] };
case 3: return { top: values[0], right: values[1], bottom: values[2], left: values[1] };
case 4: return { top: values[0], right: values[1], bottom: values[2], left: values[3] };
default: return { top: 0, right: 0, bottom: 0, left: 0 };
}
}
/**
* Create a DOMRectReadOnly-like object
*/
createDOMRectReadOnly(rect) {
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
toJSON: () => ({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left
})
};
}
}
// Add static method for feature detection
IntersectionObserverPolyfill.isSupported = () => {
return typeof IntersectionObserver !== 'undefined';
};
/**
* Observer Manager for ProteusJS
* Manages ResizeObserver and IntersectionObserver instances efficiently
*/
class ObserverManager {
constructor() {
this.resizeObservers = new Map();
this.intersectionObservers = new Map();
this.resizeEntries = new Map();
this.intersectionEntries = new Map();
this.isPolyfillMode = false;
this.checkPolyfillNeeds();
}
/**
* Observe element for resize changes
*/
observeResize(element, callback, options) {
const observerKey = this.getResizeObserverKey(options);
let observer = this.resizeObservers.get(observerKey);
if (!observer) {
observer = this.createResizeObserver(options);
this.resizeObservers.set(observerKey, observer);
}
// Store entry for cleanup
const entry = {
element,
callback: callback,
...(options && { options })
};
this.resizeEntries.set(element, entry);
// Start observing
observer.observe(element, options);
// Return unobserve function
return () => this.unobserveResize(element);
}
/**
* Observe element for intersection changes
*/
observeIntersection(element, callback, options) {
const observerKey = this.getIntersectionObserverKey(options);
let observer = this.intersectionObservers.get(observerKey);
if (!observer) {
observer = this.createIntersectionObserver(callback, options);
this.intersectionObservers.set(observerKey, observer);
}
// Store entry for cleanup
const entry = {
element,
callback: callback,
...(options && { options })
};
this.intersectionEntries.set(element, entry);
// Start observing
observer.observe(element);
// Return unobserve function
return () => this.unobserveIntersection(element);
}
/**
* Stop observing element for resize changes
*/
unobserveResize(element) {
const entry = this.resizeEntries.get(element);
if (!entry)
return;
const observerKey = this.getResizeObserverKey(entry.options);
const observer = this.resizeObservers.get(observerKey);
if (observer) {
observer.unobserve(element);
}
this.resizeEntries.delete(element);
this.cleanupResizeObserver(observerKey);
}
/**
* Stop observing element for intersection changes
*/
unobserveIntersection(element) {
const entry = this.intersectionEntries.get(element);
if (!entry)
return;
const observerKey = this.getIntersectionObserverKey(entry.options);
const observer = this.intersectionObservers.get(observerKey);
if (observer) {
observer.unobserve(element);
}
this.intersectionEntries.delete(element);
this.cleanupIntersectionObserver(observerKey);
}
/**
* Get total number of observed elements
*/
getObservedElementCount() {
return this.resizeEntries.size + this.intersectionEntries.size;
}
/**
* Get number of active observers
*/
getObserverCount() {
return this.resizeObservers.size + this.intersectionObservers.size;
}
/**
* Check if element is being observed for resize
*/
isObservingResize(element) {
return this.resizeEntries.has(element);
}
/**
* Check if element is being observed for intersection
*/
isObservingIntersection(element) {
return this.intersectionEntries.has(element);
}
/**
* Disconnect all observers and clean up
*/
destroy() {
// Disconnect all resize observers
this.resizeObservers.forEach(observer => observer.disconnect());
this.resizeObservers.clear();
this.resizeEntries.clear();
// Disconnect all intersection observers
this.intersectionObservers.forEach(observer => observer.disconnect());
this.intersectionObservers.clear();
this.intersectionEntries.clear();
}
/**
* Get debug information
*/
getDebugInfo() {
return {
isPolyfillMode: this.isPolyfillMode,
resizeObservers: this.resizeObservers.size,
intersectionObservers: this.intersectionObservers.size,
resizeEntries: this.resizeEntries.size,
intersectionEntries: this.intersectionEntries.size,
totalObservedElements: this.getObservedElementCount(),
totalObservers: this.getObserverCount()
};
}
/**
* Check if polyfills are needed and set up accordingly
*/
checkPolyfillNeeds() {
if (typeof ResizeObserver === 'undefined') {
this.setupResizeObserverPolyfill();
this.isPolyfillMode = true;
}
if (typeof IntersectionObserver === 'undefined') {
this.setupIntersectionObserverPolyfill();
this.isPolyfillMode = true;
}
}
/**
* Set up ResizeObserver polyfill
*/
setupResizeObserverPolyfill() {
globalThis.ResizeObserver = ResizeObserverPolyfill;
}
/**
* Set up IntersectionObserver polyfill
*/
setupIntersectionObserverPolyfill() {
globalThis.IntersectionObserver = IntersectionObserverPolyfill;
}
/**
* Create a new ResizeObserver instance
*/
createResizeObserver(_options) {
return new ResizeObserver((entries) => {
entries.forEach(entry => {
const storedEntry = this.resizeEntries.get(entry.target);
if (storedEntry) {
storedEntry.callback(entry);
}
});
});
}
/**
* Create a new IntersectionObserver instance
*/
createIntersectionObserver(_callback, options) {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
const storedEntry = this.intersectionEntries.get(entry.target);
if (storedEntry) {
storedEntry.callback(entry);
}
});
}, options);
}
/**
* Generate key for ResizeObserver based on options
*/
getResizeObserverKey(options) {
if (!options)
return 'default';
return `box:${options.box || 'content-box'}`;
}
/**
* Generate key for IntersectionObserver based on options
*/
getIntersectionObserverKey(options) {
if (!options)
return 'default';
const root = options.root ? 'custom' : 'viewport';
const rootMargin = options.rootMargin || '0px';
const threshold = Array.isArray(options.threshold)
? options.threshold.join(',')
: (options.threshold || 0).toString();
return `${root}:${rootMargin}:${threshold}`;
}
/**
* Clean up ResizeObserver if no longer needed
*/
cleanupResizeObserver(observerKey) {
const hasElements = Array.from(this.resizeEntries.values()).some(entry => this.getResizeObserverKey(entry.options) === observerKey);
if (!hasElements) {
const observer = this.resizeObservers.get(observerKey);
if (observer) {
observer.disconnect();
this.resizeObservers.delete(observerKey);
}
}
}
/**
* Clean up IntersectionObserver if no longer needed
*/
cleanupIntersectionObserver(observerKey) {
const hasElements = Array.from(this.intersectionEntries.values()).some(entry => this.getIntersectionObserverKey(entry.options) === observerKey);
if (!hasElements) {
const observer = this.intersectionObservers.get(observerKey);
if (observer) {
observer.disconnect();
this.intersectionObservers.delete(observerKey);
}
}
}
}
/**
* Debounce and throttle utilities for ProteusJS
* Optimizes performance by controlling function execution frequency
*/
/**
* Debounce function execution
*/
function debounce(func, wait, options = {}) {
const { leading = false, trailing = true, maxWait } = options;
let timeoutId = null;
let lastCallTime;
let lastInvokeTime = 0;
let lastArgs;
let lastThis;
let result;
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = undefined;
lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
lastInvokeTime = time;
timeoutId = window.setTimeout(timerExpired, wait);
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxWait !== undefined
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait));
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timeoutId = window.setTimeout(timerExpired, remainingWait(time));
return undefined;
}
function trailingEdge(time) {
timeoutId = null;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = undefined;
lastThis = undefined;
return result;
}
function cancel() {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastInvokeTime = 0;
lastArgs = undefined;
lastCallTime = undefined;
lastThis = undefined;
}
function flush() {
return timeoutId === null ? result : trailingEdge(Date.now());
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === null) {
return leadingEdge(lastCallTime);
}
if (maxWait !== undefined) {
timeoutId = window.setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timeoutId === null) {
timeoutId = window.setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
/**
* Performance utilities for ProteusJS
* Provides timing, measurement, and optimization tools
*/
class PerformanceTracker {
constructor(budget) {
this.marks = new Map();
this.measurements = [];
this.warningThreshold = 0.8; // 80% of budget
this.budget = {
responseTime: 60, // 60ms default
frameRate: 60, // 60fps default
memoryUsage: 100, // 100MB default
...budget
};
}
/**
* Start timing a performance mark
*/
mark(name, metadata) {
const mark = {
name,
startTime: performance.now(),
...(metadata && { metadata })
};
this.marks.set(name, mark);
// Use Performance API if available
if (typeof performance.mark === 'function') {
performance.mark(`proteus-${name}-start`);
}
}
/**
* End timing a performance mark
*/
measure(name) {
const mark = this.marks.get(name);
if (!mark) {
console.warn(`ProteusJS: Performance mark "${name}" not found`);
return null;
}
const endTime = performance.now();
const duration = endTime - mark.startTime;
const measurement = {
...mark,
endTime,
duration
};
this.measurements.push(measurement);
this.marks.delete(name);
// Use Performance API if available
if (typeof performance.mark === 'function' && typeof performance.measure === 'function') {
performance.mark(`proteus-${name}-end`);
performance.measure(`proteus-${name}`, `proteus-${name}-start`, `proteus-${name}-end`);
}
// Check against budget
this.checkBudget(measurement);
return measurement;
}
/**
* Get all measurements
*/
getMeasurements() {
return [...this.measurements];
}
/**
* Get measurements by name pattern
*/
getMeasurementsByPattern(pattern) {
return this.measurements.filter(m => pattern.test(m.name));
}
/**
* Get average duration for measurements with the same name
*/
getAverageDuration(name) {
const matching = this.measurements.filter(m => m.name === name && m.duration !== undefined);
if (matching.length === 0)
return 0;
const total = matching.reduce((sum, m) => sum + (m.duration || 0), 0);
return total / matching.length;
}
/**
* Get performance statistics
*/
getStats() {
const stats = {
totalMeasurements: this.measurements.length,
activeMeasurements: this.marks.size,
budget: this.budget,
violations: this.getBudgetViolations()
};
// Group by name
const byName = {};
this.measurements.forEach(m => {