mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
Fix: Price Tiles volume/btc display + Better memory clean / tooltip manager.
This commit is contained in:
@@ -260,30 +260,80 @@ const ApiManager = (function() {
|
|||||||
fetchVolumeData: async function() {
|
fetchVolumeData: async function() {
|
||||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||||
try {
|
try {
|
||||||
const coins = (window.config && window.config.coins) ?
|
let coinList = (window.config && window.config.coins) ?
|
||||||
window.config.coins
|
window.config.coins
|
||||||
.filter(coin => coin.usesCoinGecko)
|
.filter(coin => coin.usesCoinGecko)
|
||||||
.map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
|
.map(coin => {
|
||||||
|
return window.config.getCoinBackendId ?
|
||||||
|
window.config.getCoinBackendId(coin.name) :
|
||||||
|
(typeof getCoinBackendId === 'function' ?
|
||||||
|
getCoinBackendId(coin.name) : coin.name.toLowerCase());
|
||||||
|
})
|
||||||
.join(',') :
|
.join(',') :
|
||||||
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
|
||||||
|
|
||||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
|
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
|
||||||
|
coinList = coinList + ',zcoin';
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
|
||||||
|
|
||||||
const response = await this.makePostRequest(url, {
|
const response = await this.makePostRequest(url, {
|
||||||
'User-Agent': 'Mozilla/5.0',
|
'User-Agent': 'Mozilla/5.0',
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response || typeof response !== 'object') {
|
||||||
|
throw new Error('Invalid response from CoinGecko API');
|
||||||
|
}
|
||||||
|
|
||||||
const volumeData = {};
|
const volumeData = {};
|
||||||
|
|
||||||
Object.entries(response).forEach(([coinId, data]) => {
|
Object.entries(response).forEach(([coinId, data]) => {
|
||||||
if (data && data.usd_24h_vol) {
|
if (data && data.usd_24h_vol !== undefined) {
|
||||||
volumeData[coinId] = {
|
volumeData[coinId] = {
|
||||||
total_volume: data.usd_24h_vol,
|
total_volume: data.usd_24h_vol || 0,
|
||||||
price_change_percentage_24h: data.usd_24h_change || 0
|
price_change_percentage_24h: data.usd_24h_change || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const coinMappings = {
|
||||||
|
'firo': ['firo', 'zcoin'],
|
||||||
|
'zcoin': ['zcoin', 'firo'],
|
||||||
|
'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
|
||||||
|
'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
|
||||||
|
'particl': ['particl', 'part']
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
|
||||||
|
volumeData['firo'] = {
|
||||||
|
total_volume: response['zcoin'].usd_24h_vol || 0,
|
||||||
|
price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
|
||||||
|
volumeData['bitcoincash'] = {
|
||||||
|
total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
|
||||||
|
price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
|
||||||
|
if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
|
||||||
|
for (const altId of alternativeIds) {
|
||||||
|
if (response[altId] && response[altId].usd_24h_vol) {
|
||||||
|
volumeData[mainCoin] = {
|
||||||
|
total_volume: response[altId].usd_24h_vol,
|
||||||
|
price_change_percentage_24h: response[altId].usd_24h_change || 0
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return volumeData;
|
return volumeData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching volume data:", error);
|
console.error("Error fetching volume data:", error);
|
||||||
@@ -364,7 +414,6 @@ const ApiManager = (function() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dispose: function() {
|
dispose: function() {
|
||||||
// Clear any pending requests or resources
|
|
||||||
rateLimiter.requestQueue = {};
|
rateLimiter.requestQueue = {};
|
||||||
rateLimiter.lastRequestTime = {};
|
rateLimiter.lastRequestTime = {};
|
||||||
state.isInitialized = false;
|
state.isInitialized = false;
|
||||||
|
|||||||
@@ -1,219 +1,663 @@
|
|||||||
const MemoryManager = (function() {
|
const MemoryManager = (function() {
|
||||||
|
const config = {
|
||||||
|
tooltipCleanupInterval: 60000,
|
||||||
|
maxTooltipsThreshold: 100,
|
||||||
|
diagnosticsInterval: 300000,
|
||||||
|
tooltipLifespan: 240000,
|
||||||
|
debug: false,
|
||||||
|
autoCleanup: true,
|
||||||
|
elementVerificationInterval: 50000,
|
||||||
|
tooltipSelectors: [
|
||||||
|
'[data-tippy-root]',
|
||||||
|
'[data-tooltip-trigger-id]',
|
||||||
|
'.tooltip',
|
||||||
|
'.tippy-box',
|
||||||
|
'.tippy-content'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mutationObserver = null;
|
||||||
|
|
||||||
|
const safeGet = (obj, path, defaultValue = null) => {
|
||||||
|
if (!obj) return defaultValue;
|
||||||
|
const pathParts = path.split('.');
|
||||||
|
let result = obj;
|
||||||
|
for (const part of pathParts) {
|
||||||
|
if (result === null || result === undefined) return defaultValue;
|
||||||
|
result = result[part];
|
||||||
|
}
|
||||||
|
return result !== undefined ? result : defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
isMonitoringEnabled: false,
|
intervals: new Map(),
|
||||||
monitorInterval: null,
|
trackedTooltips: new Map(),
|
||||||
cleanupInterval: null
|
trackedElements: new WeakMap(),
|
||||||
|
startTime: Date.now(),
|
||||||
|
lastCleanupTime: Date.now(),
|
||||||
|
metrics: {
|
||||||
|
tooltipsCreated: 0,
|
||||||
|
tooltipsDestroyed: 0,
|
||||||
|
orphanedTooltipsRemoved: 0,
|
||||||
|
elementsProcessed: 0,
|
||||||
|
cleanupRuns: 0,
|
||||||
|
manualCleanupRuns: 0,
|
||||||
|
lastMemoryUsage: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const log = (message, ...args) => {
|
||||||
monitorInterval: 30000,
|
if (!config.debug) return;
|
||||||
cleanupInterval: 60000,
|
const now = new Date().toISOString();
|
||||||
debug: false
|
console.log(`[MemoryManager ${now}]`, message, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
function log(message, ...args) {
|
const logError = (message, error) => {
|
||||||
if (config.debug) {
|
console.error(`[MemoryManager] ${message}`, error);
|
||||||
console.log(`[MemoryManager] ${message}`, ...args);
|
};
|
||||||
|
|
||||||
|
const trackTooltip = (element, tooltipInstance) => {
|
||||||
|
try {
|
||||||
|
if (!element || !tooltipInstance) return;
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id') || `tooltip_${timestamp}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
|
||||||
|
state.trackedTooltips.set(tooltipId, {
|
||||||
|
timestamp,
|
||||||
|
element,
|
||||||
|
instance: tooltipInstance,
|
||||||
|
processed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
state.metrics.tooltipsCreated++;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state.trackedTooltips.has(tooltipId)) {
|
||||||
|
destroyTooltip(tooltipId);
|
||||||
}
|
}
|
||||||
|
}, config.tooltipLifespan);
|
||||||
|
|
||||||
|
return tooltipId;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error tracking tooltip:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroyTooltip = (tooltipId) => {
|
||||||
|
try {
|
||||||
|
const tooltipInfo = state.trackedTooltips.get(tooltipId);
|
||||||
|
if (!tooltipInfo) return false;
|
||||||
|
|
||||||
|
const { element, instance } = tooltipInfo;
|
||||||
|
|
||||||
|
if (instance && typeof instance.destroy === 'function') {
|
||||||
|
instance.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicAPI = {
|
if (element && element.removeAttribute) {
|
||||||
enableMonitoring: function(interval = config.monitorInterval) {
|
element.removeAttribute('data-tooltip-trigger-id');
|
||||||
if (state.monitorInterval) {
|
element.removeAttribute('aria-describedby');
|
||||||
clearInterval(state.monitorInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.isMonitoringEnabled = true;
|
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
|
||||||
config.monitorInterval = interval;
|
if (tippyRoot && tippyRoot.parentNode) {
|
||||||
|
tippyRoot.parentNode.removeChild(tippyRoot);
|
||||||
|
}
|
||||||
|
|
||||||
this.logMemoryUsage();
|
state.trackedTooltips.delete(tooltipId);
|
||||||
|
state.metrics.tooltipsDestroyed++;
|
||||||
|
|
||||||
state.monitorInterval = setInterval(() => {
|
|
||||||
this.logMemoryUsage();
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
|
|
||||||
return true;
|
return true;
|
||||||
},
|
} catch (error) {
|
||||||
|
logError(`Error destroying tooltip ${tooltipId}:`, error);
|
||||||
disableMonitoring: function() {
|
return false;
|
||||||
if (state.monitorInterval) {
|
|
||||||
clearInterval(state.monitorInterval);
|
|
||||||
state.monitorInterval = null;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
state.isMonitoringEnabled = false;
|
const removeOrphanedTooltips = () => {
|
||||||
console.log('Memory monitoring disabled');
|
try {
|
||||||
return true;
|
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
|
||||||
},
|
let removed = 0;
|
||||||
|
|
||||||
logMemoryUsage: function() {
|
tippyRoots.forEach(root => {
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const tooltipId = root.getAttribute('data-for-tooltip-id');
|
||||||
console.log(`=== Memory Monitor [${timestamp}] ===`);
|
|
||||||
|
|
||||||
|
const trigger = tooltipId ?
|
||||||
|
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (!trigger || !document.body.contains(trigger)) {
|
||||||
|
if (root.parentNode) {
|
||||||
|
root.parentNode.removeChild(root);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => {
|
||||||
|
const tooltipId = trigger.getAttribute('data-tooltip-trigger-id');
|
||||||
|
const root = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
trigger.removeAttribute('data-tooltip-trigger-id');
|
||||||
|
trigger.removeAttribute('aria-describedby');
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
state.metrics.orphanedTooltipsRemoved += removed;
|
||||||
|
return removed;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error removing orphaned tooltips:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkMemoryUsage = () => {
|
||||||
if (window.performance && window.performance.memory) {
|
if (window.performance && window.performance.memory) {
|
||||||
console.log('Memory usage:', {
|
const memoryUsage = {
|
||||||
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
usedJSHeapSize: window.performance.memory.usedJSHeapSize,
|
||||||
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
|
totalJSHeapSize: window.performance.memory.totalJSHeapSize,
|
||||||
});
|
jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit,
|
||||||
|
percentUsed: (window.performance.memory.usedJSHeapSize / window.performance.memory.jsHeapSizeLimit * 100).toFixed(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
state.metrics.lastMemoryUsage = memoryUsage;
|
||||||
|
return memoryUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigator.deviceMemory) {
|
return null;
|
||||||
console.log('Device memory:', navigator.deviceMemory, 'GB');
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const nodeCount = document.querySelectorAll('*').length;
|
const checkForDisconnectedElements = () => {
|
||||||
console.log('DOM node count:', nodeCount);
|
try {
|
||||||
|
const disconnectedElements = new Set();
|
||||||
|
|
||||||
if (window.CleanupManager) {
|
state.trackedTooltips.forEach((info, id) => {
|
||||||
const counts = CleanupManager.getResourceCounts();
|
const { element } = info;
|
||||||
console.log('Managed resources:', counts);
|
if (element && !document.body.contains(element)) {
|
||||||
}
|
disconnectedElements.add(id);
|
||||||
|
|
||||||
if (window.TooltipManager) {
|
|
||||||
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
|
|
||||||
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
|
||||||
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.CacheManager && window.CacheManager.getStats) {
|
|
||||||
const cacheStats = CacheManager.getStats();
|
|
||||||
console.log('Cache stats:', cacheStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.IdentityManager && window.IdentityManager.getStats) {
|
|
||||||
const identityStats = window.IdentityManager.getStats();
|
|
||||||
console.log('Identity cache stats:', identityStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('==============================');
|
|
||||||
},
|
|
||||||
|
|
||||||
enableAutoCleanup: function(interval = config.cleanupInterval) {
|
|
||||||
if (state.cleanupInterval) {
|
|
||||||
clearInterval(state.cleanupInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.cleanupInterval = interval;
|
|
||||||
|
|
||||||
this.forceCleanup();
|
|
||||||
|
|
||||||
state.cleanupInterval = setInterval(() => {
|
|
||||||
this.forceCleanup();
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
log('Auto-cleanup enabled every', interval/1000, 'seconds');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
disableAutoCleanup: function() {
|
|
||||||
if (state.cleanupInterval) {
|
|
||||||
clearInterval(state.cleanupInterval);
|
|
||||||
state.cleanupInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Memory auto-cleanup disabled');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
forceCleanup: function() {
|
|
||||||
if (config.debug) {
|
|
||||||
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.CacheManager && CacheManager.cleanup) {
|
|
||||||
CacheManager.cleanup(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.TooltipManager && TooltipManager.cleanup) {
|
|
||||||
window.TooltipManager.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
|
||||||
if (window.TooltipManager && TooltipManager.destroy) {
|
|
||||||
window.TooltipManager.destroy(element);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.chartModule && chartModule.cleanup) {
|
disconnectedElements.forEach(id => {
|
||||||
chartModule.cleanup();
|
destroyTooltip(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return disconnectedElements.size;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error checking for disconnected elements:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupMutationObserver = () => {
|
||||||
|
if (mutationObserver) {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationObserver = new MutationObserver(mutations => {
|
||||||
|
let needsCleanup = false;
|
||||||
|
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (mutation.removedNodes.length) {
|
||||||
|
Array.from(mutation.removedNodes).forEach(node => {
|
||||||
|
if (node.nodeType === 1) {
|
||||||
|
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
|
||||||
|
const tooltipId = node.getAttribute('data-tooltip-trigger-id');
|
||||||
|
destroyTooltip(tooltipId);
|
||||||
|
needsCleanup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.querySelectorAll) {
|
||||||
|
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
|
||||||
|
if (tooltipTriggers.length > 0) {
|
||||||
|
tooltipTriggers.forEach(el => {
|
||||||
|
const tooltipId = el.getAttribute('data-tooltip-trigger-id');
|
||||||
|
destroyTooltip(tooltipId);
|
||||||
|
});
|
||||||
|
needsCleanup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needsCleanup) {
|
||||||
|
removeOrphanedTooltips();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mutationObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutationObserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
const performCleanup = (force = false) => {
|
||||||
|
try {
|
||||||
|
log('Starting tooltip cleanup' + (force ? ' (forced)' : ''));
|
||||||
|
|
||||||
|
state.lastCleanupTime = Date.now();
|
||||||
|
state.metrics.cleanupRuns++;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
state.metrics.manualCleanupRuns++;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-tippy-root]').forEach(root => {
|
||||||
|
const instance = safeGet(root, '_tippy');
|
||||||
|
if (instance && instance._animationFrame) {
|
||||||
|
cancelAnimationFrame(instance._animationFrame);
|
||||||
|
instance._animationFrame = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const orphanedRemoved = removeOrphanedTooltips();
|
||||||
|
|
||||||
|
const disconnectedRemoved = checkForDisconnectedElements();
|
||||||
|
|
||||||
|
const tooltipCount = document.querySelectorAll('[data-tippy-root]').length;
|
||||||
|
const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
||||||
|
|
||||||
|
if (force || tooltipCount > config.maxTooltipsThreshold) {
|
||||||
|
if (tooltipCount > config.maxTooltipsThreshold) {
|
||||||
|
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => {
|
||||||
|
const tooltipId = trigger.getAttribute('data-tooltip-trigger-id');
|
||||||
|
destroyTooltip(tooltipId);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-tippy-root]').forEach(root => {
|
||||||
|
if (root.parentNode) {
|
||||||
|
root.parentNode.removeChild(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-tooltip-trigger-id], [aria-describedby]').forEach(el => {
|
||||||
|
if (window.CleanupManager && window.CleanupManager.removeListenersByElement) {
|
||||||
|
window.CleanupManager.removeListenersByElement(el);
|
||||||
|
} else {
|
||||||
|
if (el.parentNode) {
|
||||||
|
const clone = el.cloneNode(true);
|
||||||
|
el.parentNode.replaceChild(clone, el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.gc) {
|
if (window.gc) {
|
||||||
window.gc();
|
window.gc();
|
||||||
} else {
|
} else if (force) {
|
||||||
const arr = new Array(1000);
|
const arr = new Array(1000);
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
arr[i] = new Array(10000).join('x');
|
arr[i] = new Array(10000).join('x');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.debug) {
|
checkMemoryUsage();
|
||||||
console.log('Memory cleanup completed');
|
|
||||||
|
const result = {
|
||||||
|
orphanedRemoved,
|
||||||
|
disconnectedRemoved,
|
||||||
|
tooltipCount: document.querySelectorAll('[data-tippy-root]').length,
|
||||||
|
triggerCount: document.querySelectorAll('[data-tooltip-trigger-id]').length,
|
||||||
|
memoryUsage: state.metrics.lastMemoryUsage
|
||||||
|
};
|
||||||
|
|
||||||
|
log('Cleanup completed', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error during cleanup:', error);
|
||||||
|
return { error: error.message };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runDiagnostics = () => {
|
||||||
|
try {
|
||||||
|
log('Running memory diagnostics');
|
||||||
|
|
||||||
|
const memoryUsage = checkMemoryUsage();
|
||||||
|
const tooltipCount = document.querySelectorAll('[data-tippy-root]').length;
|
||||||
|
const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
||||||
|
|
||||||
|
const diagnostics = {
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
uptime: Date.now() - state.startTime,
|
||||||
|
memoryUsage,
|
||||||
|
elementsCount: {
|
||||||
|
tippyRoots: tooltipCount,
|
||||||
|
tooltipTriggers: triggerCount,
|
||||||
|
orphanedTriggers: triggerCount - tooltipCount > 0 ? triggerCount - tooltipCount : 0,
|
||||||
|
orphanedTooltips: tooltipCount - triggerCount > 0 ? tooltipCount - triggerCount : 0
|
||||||
|
},
|
||||||
|
metrics: { ...state.metrics },
|
||||||
|
issues: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tooltipCount > config.maxTooltipsThreshold) {
|
||||||
|
diagnostics.issues.push({
|
||||||
|
severity: 'high',
|
||||||
|
message: `Excessive tooltip count: ${tooltipCount} (threshold: ${config.maxTooltipsThreshold})`,
|
||||||
|
recommendation: 'Run cleanup and check for tooltip creation loops'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(tooltipCount - triggerCount) > 10) {
|
||||||
|
diagnostics.issues.push({
|
||||||
|
severity: 'medium',
|
||||||
|
message: `Mismatch between tooltips (${tooltipCount}) and triggers (${triggerCount})`,
|
||||||
|
recommendation: 'Remove orphaned tooltips and tooltip triggers'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryUsage && memoryUsage.percentUsed > 80) {
|
||||||
|
diagnostics.issues.push({
|
||||||
|
severity: 'high',
|
||||||
|
message: `High memory usage: ${memoryUsage.percentUsed}%`,
|
||||||
|
recommendation: 'Force garbage collection and check for memory leaks'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.autoCleanup && diagnostics.issues.some(issue => issue.severity === 'high')) {
|
||||||
|
log('Critical issues detected, triggering automatic cleanup');
|
||||||
|
performCleanup(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error running diagnostics:', error);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchTooltipManager = () => {
|
||||||
|
try {
|
||||||
|
if (!window.TooltipManager) {
|
||||||
|
log('TooltipManager not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Patching TooltipManager');
|
||||||
|
|
||||||
|
const originalCreate = window.TooltipManager.create;
|
||||||
|
const originalDestroy = window.TooltipManager.destroy;
|
||||||
|
const originalCleanup = window.TooltipManager.cleanup;
|
||||||
|
|
||||||
|
window.TooltipManager.create = function(element, content, options = {}) {
|
||||||
|
if (!element) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = originalCreate.call(this, element, content, options);
|
||||||
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||||
|
|
||||||
|
if (tooltipId) {
|
||||||
|
const tippyInstance = safeGet(element, '_tippy') || null;
|
||||||
|
trackTooltip(element, tippyInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error in patched create:', error);
|
||||||
|
return originalCreate.call(this, element, content, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.TooltipManager.destroy = function(element) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||||
|
|
||||||
|
originalDestroy.call(this, element);
|
||||||
|
|
||||||
|
if (tooltipId) {
|
||||||
|
state.trackedTooltips.delete(tooltipId);
|
||||||
|
state.metrics.tooltipsDestroyed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error in patched destroy:', error);
|
||||||
|
originalDestroy.call(this, element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.TooltipManager.cleanup = function() {
|
||||||
|
try {
|
||||||
|
originalCleanup.call(this);
|
||||||
|
removeOrphanedTooltips();
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error in patched cleanup:', error);
|
||||||
|
originalCleanup.call(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
} catch (error) {
|
||||||
|
logError('Error patching TooltipManager:', error);
|
||||||
setDebugMode: function(enabled) {
|
return false;
|
||||||
config.debug = Boolean(enabled);
|
|
||||||
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
getStatus: function() {
|
|
||||||
return {
|
|
||||||
monitoring: {
|
|
||||||
enabled: Boolean(state.monitorInterval),
|
|
||||||
interval: config.monitorInterval
|
|
||||||
},
|
|
||||||
autoCleanup: {
|
|
||||||
enabled: Boolean(state.cleanupInterval),
|
|
||||||
interval: config.cleanupInterval
|
|
||||||
},
|
|
||||||
debug: config.debug
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options = {}) {
|
|
||||||
if (options.debug !== undefined) {
|
|
||||||
this.setDebugMode(options.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.enableMonitoring) {
|
|
||||||
this.enableMonitoring(options.monitorInterval || config.monitorInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.enableAutoCleanup) {
|
|
||||||
this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.CleanupManager) {
|
|
||||||
window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
|
|
||||||
}
|
|
||||||
|
|
||||||
log('MemoryManager initialized');
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
dispose: function() {
|
|
||||||
this.disableMonitoring();
|
|
||||||
this.disableAutoCleanup();
|
|
||||||
log('MemoryManager disposed');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return publicAPI;
|
const patchTippy = () => {
|
||||||
})();
|
try {
|
||||||
|
if (typeof tippy !== 'function') {
|
||||||
|
log('tippy.js not found globally');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
window.MemoryManager = MemoryManager;
|
log('Patching global tippy');
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
const originalTippy = window.tippy;
|
||||||
if (!window.memoryManagerInitialized) {
|
|
||||||
MemoryManager.initialize();
|
window.tippy = function(...args) {
|
||||||
window.memoryManagerInitialized = true;
|
const result = originalTippy.apply(this, args);
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result.forEach(instance => {
|
||||||
|
const reference = instance.reference;
|
||||||
|
|
||||||
|
if (reference) {
|
||||||
|
const originalShow = instance.show;
|
||||||
|
const originalHide = instance.hide;
|
||||||
|
const originalDestroy = instance.destroy;
|
||||||
|
|
||||||
|
instance.show = function(...showArgs) {
|
||||||
|
return originalShow.apply(this, showArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.hide = function(...hideArgs) {
|
||||||
|
return originalHide.apply(this, hideArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.destroy = function(...destroyArgs) {
|
||||||
|
return originalDestroy.apply(this, destroyArgs);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(window.tippy, originalTippy);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error patching tippy:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMonitoring = () => {
|
||||||
|
try {
|
||||||
|
stopMonitoring();
|
||||||
|
|
||||||
|
state.intervals.set('cleanup', setInterval(() => {
|
||||||
|
performCleanup();
|
||||||
|
}, config.tooltipCleanupInterval));
|
||||||
|
|
||||||
|
state.intervals.set('diagnostics', setInterval(() => {
|
||||||
|
runDiagnostics();
|
||||||
|
}, config.diagnosticsInterval));
|
||||||
|
|
||||||
|
state.intervals.set('elementVerification', setInterval(() => {
|
||||||
|
checkForDisconnectedElements();
|
||||||
|
}, config.elementVerificationInterval));
|
||||||
|
|
||||||
|
setupMutationObserver();
|
||||||
|
|
||||||
|
log('Monitoring started');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error starting monitoring:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopMonitoring = () => {
|
||||||
|
try {
|
||||||
|
state.intervals.forEach((interval, key) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.intervals.clear();
|
||||||
|
|
||||||
|
if (mutationObserver) {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
mutationObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Monitoring stopped');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error stopping monitoring:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoFix = () => {
|
||||||
|
try {
|
||||||
|
log('Running auto-fix');
|
||||||
|
|
||||||
|
performCleanup(true);
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
||||||
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||||
|
const duplicates = document.querySelectorAll(`[data-tooltip-trigger-id="${tooltipId}"]`);
|
||||||
|
|
||||||
|
if (duplicates.length > 1) {
|
||||||
|
for (let i = 1; i < duplicates.length; i++) {
|
||||||
|
duplicates[i].removeAttribute('data-tooltip-trigger-id');
|
||||||
|
duplicates[i].removeAttribute('aria-describedby');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
|
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
|
||||||
|
tippyRoots.forEach(root => {
|
||||||
|
if (!document.body.contains(root) && root.parentNode) {
|
||||||
|
root.parentNode.removeChild(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.TooltipManager && window.TooltipManager.getInstance) {
|
||||||
|
const manager = window.TooltipManager.getInstance();
|
||||||
|
if (manager && manager.chartRefs && manager.chartRefs.clear) {
|
||||||
|
manager.chartRefs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager && manager.tooltipElementsMap && manager.tooltipElementsMap.clear) {
|
||||||
|
manager.tooltipElementsMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patchTooltipManager();
|
||||||
|
patchTippy();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error during auto-fix:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialize = (options = {}) => {
|
||||||
|
try {
|
||||||
|
Object.assign(config, options);
|
||||||
|
|
||||||
|
if (document.head) {
|
||||||
|
const metaCache = document.createElement('meta');
|
||||||
|
metaCache.setAttribute('http-equiv', 'Cache-Control');
|
||||||
|
metaCache.setAttribute('content', 'no-store, max-age=0');
|
||||||
|
document.head.appendChild(metaCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
patchTooltipManager();
|
||||||
|
patchTippy();
|
||||||
|
|
||||||
|
startMonitoring();
|
||||||
|
|
||||||
|
if (window.CleanupManager && window.CleanupManager.registerResource) {
|
||||||
|
window.CleanupManager.registerResource('memorymanager', MemoryManager, (optimizer) => {
|
||||||
|
optimizer.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Memory Optimizer initialized', config);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
runDiagnostics();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return MemoryManager;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error initializing Memory Optimizer:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispose = () => {
|
||||||
|
try {
|
||||||
|
log('Disposing Memory Optimizer');
|
||||||
|
|
||||||
|
performCleanup(true);
|
||||||
|
|
||||||
|
stopMonitoring();
|
||||||
|
|
||||||
|
state.trackedTooltips.clear();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Error disposing Memory Optimizer:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize,
|
||||||
|
dispose,
|
||||||
|
performCleanup,
|
||||||
|
runDiagnostics,
|
||||||
|
autoFix,
|
||||||
|
getConfig: () => ({ ...config }),
|
||||||
|
getMetrics: () => ({ ...state.metrics }),
|
||||||
|
setDebugMode: (enabled) => {
|
||||||
|
config.debug = Boolean(enabled);
|
||||||
|
return config.debug;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
MemoryManager.initialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MemoryManager = MemoryManager;
|
||||||
console.log('Memory Manager initialized');
|
console.log('Memory Manager initialized');
|
||||||
|
|||||||
@@ -1,46 +1,52 @@
|
|||||||
const TooltipManager = (function() {
|
const TooltipManager = (function() {
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
|
const tooltipInstanceMap = new WeakMap();
|
||||||
|
|
||||||
class TooltipManagerImpl {
|
class TooltipManagerImpl {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeTooltips = new WeakMap();
|
|
||||||
this.tooltipIdCounter = 0;
|
|
||||||
this.pendingAnimationFrames = new Set();
|
this.pendingAnimationFrames = new Set();
|
||||||
this.tooltipElementsMap = new Map();
|
this.pendingTimeouts = new Set();
|
||||||
this.maxTooltips = 300;
|
this.tooltipIdCounter = 0;
|
||||||
this.cleanupThreshold = 1.3;
|
this.maxTooltips = 200;
|
||||||
|
this.cleanupThreshold = 1.2;
|
||||||
this.disconnectedCheckInterval = null;
|
this.disconnectedCheckInterval = null;
|
||||||
|
this.cleanupInterval = null;
|
||||||
|
this.mutationObserver = null;
|
||||||
|
this.debug = false;
|
||||||
|
this.tooltipData = new WeakMap();
|
||||||
this.setupStyles();
|
this.setupStyles();
|
||||||
this.setupCleanupEvents();
|
this.setupMutationObserver();
|
||||||
this.initializeMutationObserver();
|
this.startPeriodicCleanup();
|
||||||
this.startDisconnectedElementsCheck();
|
this.startDisconnectedElementsCheck();
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(message, ...args) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`[TooltipManager] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
create(element, content, options = {}) {
|
create(element, content, options = {}) {
|
||||||
if (!element) return null;
|
if (!element || !document.body.contains(element)) return null;
|
||||||
|
|
||||||
|
if (!document.contains(element)) {
|
||||||
|
this.log('Tried to create tooltip for detached element');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
this.destroy(element);
|
this.destroy(element);
|
||||||
|
|
||||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
const currentTooltipCount = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
||||||
const oldestEntries = Array.from(this.tooltipElementsMap.entries())
|
if (currentTooltipCount > this.maxTooltips * this.cleanupThreshold) {
|
||||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
this.cleanupOrphanedTooltips();
|
||||||
.slice(0, 20);
|
this.performPeriodicCleanup(true);
|
||||||
|
|
||||||
oldestEntries.forEach(([el]) => {
|
|
||||||
this.destroy(el);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalContent = content;
|
|
||||||
|
|
||||||
const rafId = requestAnimationFrame(() => {
|
const rafId = requestAnimationFrame(() => {
|
||||||
this.pendingAnimationFrames.delete(rafId);
|
this.pendingAnimationFrames.delete(rafId);
|
||||||
|
|
||||||
@@ -48,23 +54,34 @@ const TooltipManager = (function() {
|
|||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
if (rect.width > 0 && rect.height > 0) {
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
this.createTooltip(element, originalContent, options, rect);
|
this.createTooltipInstance(element, content, options);
|
||||||
} else {
|
} else {
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
|
const maxRetries = 3;
|
||||||
|
|
||||||
const retryCreate = () => {
|
const retryCreate = () => {
|
||||||
const newRect = element.getBoundingClientRect();
|
const newRect = element.getBoundingClientRect();
|
||||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) {
|
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
|
||||||
if (newRect.width > 0 && newRect.height > 0) {
|
if (newRect.width > 0 && newRect.height > 0) {
|
||||||
this.createTooltip(element, originalContent, options, newRect);
|
this.createTooltipInstance(element, content, options);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.pendingTimeouts.delete(timeoutId);
|
||||||
const newRafId = requestAnimationFrame(retryCreate);
|
const newRafId = requestAnimationFrame(retryCreate);
|
||||||
this.pendingAnimationFrames.add(newRafId);
|
this.pendingAnimationFrames.add(newRafId);
|
||||||
|
}, 100);
|
||||||
|
this.pendingTimeouts.add(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const initialRetryId = requestAnimationFrame(retryCreate);
|
|
||||||
this.pendingAnimationFrames.add(initialRetryId);
|
const initialTimeoutId = setTimeout(() => {
|
||||||
|
this.pendingTimeouts.delete(initialTimeoutId);
|
||||||
|
const retryRafId = requestAnimationFrame(retryCreate);
|
||||||
|
this.pendingAnimationFrames.add(retryRafId);
|
||||||
|
}, 100);
|
||||||
|
this.pendingTimeouts.add(initialTimeoutId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,57 +89,15 @@ const TooltipManager = (function() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
createTooltip(element, content, options, rect) {
|
createTooltipInstance(element, content, options = {}) {
|
||||||
const targetId = element.getAttribute('data-tooltip-target');
|
if (!element || !document.body.contains(element) || !window.tippy) {
|
||||||
let bgClass = 'bg-gray-400';
|
|
||||||
let arrowColor = 'rgb(156 163 175)';
|
|
||||||
|
|
||||||
if (targetId?.includes('tooltip-offer-') && window.jsonData) {
|
|
||||||
try {
|
|
||||||
const offerId = targetId.split('tooltip-offer-')[1];
|
|
||||||
let actualOfferId = offerId;
|
|
||||||
|
|
||||||
if (offerId.includes('_')) {
|
|
||||||
[actualOfferId] = offerId.split('_');
|
|
||||||
}
|
|
||||||
|
|
||||||
let offer = null;
|
|
||||||
if (Array.isArray(window.jsonData)) {
|
|
||||||
for (let i = 0; i < window.jsonData.length; i++) {
|
|
||||||
const o = window.jsonData[i];
|
|
||||||
if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) {
|
|
||||||
offer = o;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offer) {
|
|
||||||
if (offer.is_revoked) {
|
|
||||||
bgClass = 'bg-red-500';
|
|
||||||
arrowColor = 'rgb(239 68 68)';
|
|
||||||
} else if (offer.is_own_offer) {
|
|
||||||
bgClass = 'bg-gray-300';
|
|
||||||
arrowColor = 'rgb(209 213 219)';
|
|
||||||
} else {
|
|
||||||
bgClass = 'bg-green-700';
|
|
||||||
arrowColor = 'rgb(21 128 61)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Error finding offer for tooltip:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof tippy !== 'function') {
|
|
||||||
console.error('Tippy.js is not loaded. Cannot create tooltip.');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = tippy(element, {
|
try {
|
||||||
|
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
|
||||||
|
|
||||||
|
const tooltipOptions = {
|
||||||
content: content,
|
content: content,
|
||||||
allowHTML: true,
|
allowHTML: true,
|
||||||
placement: options.placement || 'top',
|
placement: options.placement || 'top',
|
||||||
@@ -143,14 +118,25 @@ const TooltipManager = (function() {
|
|||||||
},
|
},
|
||||||
onMount(instance) {
|
onMount(instance) {
|
||||||
if (instance.popper && instance.popper.firstElementChild) {
|
if (instance.popper && instance.popper.firstElementChild) {
|
||||||
|
const bgClass = options.bgClass || 'bg-gray-400';
|
||||||
instance.popper.firstElementChild.classList.add(bgClass);
|
instance.popper.firstElementChild.classList.add(bgClass);
|
||||||
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
|
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
|
||||||
}
|
}
|
||||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||||
if (arrow) {
|
if (arrow) {
|
||||||
|
const arrowColor = options.arrowColor || 'rgb(156 163 175)';
|
||||||
arrow.style.setProperty('color', arrowColor, 'important');
|
arrow.style.setProperty('color', arrowColor, 'important');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onHidden(instance) {
|
||||||
|
if (!document.body.contains(element)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (instance && instance.destroy) {
|
||||||
|
instance.destroy();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
popperOptions: {
|
popperOptions: {
|
||||||
strategy: 'fixed',
|
strategy: 'fixed',
|
||||||
modifiers: [
|
modifiers: [
|
||||||
@@ -170,19 +156,27 @@ const TooltipManager = (function() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tippyInstance = window.tippy(element, tooltipOptions);
|
||||||
|
|
||||||
|
if (tippyInstance && Array.isArray(tippyInstance) && tippyInstance[0]) {
|
||||||
|
this.tooltipData.set(element, {
|
||||||
|
id: tooltipId,
|
||||||
|
instance: tippyInstance[0],
|
||||||
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
element.setAttribute('data-tooltip-trigger-id', tooltipId);
|
element.setAttribute('data-tooltip-trigger-id', tooltipId);
|
||||||
this.activeTooltips.set(element, instance);
|
|
||||||
|
|
||||||
this.tooltipElementsMap.set(element, {
|
tooltipInstanceMap.set(element, tippyInstance[0]);
|
||||||
timestamp: Date.now(),
|
|
||||||
id: tooltipId
|
|
||||||
});
|
|
||||||
|
|
||||||
return instance;
|
return tippyInstance[0];
|
||||||
} catch (e) {
|
}
|
||||||
console.error('Error creating tooltip:', e);
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating tooltip:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,35 +184,49 @@ const TooltipManager = (function() {
|
|||||||
destroy(element) {
|
destroy(element) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
const id = element.getAttribute('data-tooltip-trigger-id');
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const instance = this.activeTooltips.get(element);
|
|
||||||
if (instance?.[0]) {
|
|
||||||
try {
|
try {
|
||||||
instance[0].destroy();
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||||
} catch (e) {
|
if (!tooltipId) return;
|
||||||
console.warn('Error destroying tooltip:', e);
|
|
||||||
|
|
||||||
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
|
const tooltipData = this.tooltipData.get(element);
|
||||||
|
const instance = tooltipData?.instance || tooltipInstanceMap.get(element);
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
try {
|
||||||
|
instance.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error destroying tooltip instance:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.removeAttribute('data-tooltip-trigger-id');
|
||||||
|
element.removeAttribute('aria-describedby');
|
||||||
|
|
||||||
|
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
|
||||||
if (tippyRoot && tippyRoot.parentNode) {
|
if (tippyRoot && tippyRoot.parentNode) {
|
||||||
tippyRoot.parentNode.removeChild(tippyRoot);
|
tippyRoot.parentNode.removeChild(tippyRoot);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeTooltips.delete(element);
|
this.tooltipData.delete(element);
|
||||||
this.tooltipElementsMap.delete(element);
|
tooltipInstanceMap.delete(element);
|
||||||
|
} catch (error) {
|
||||||
element.removeAttribute('data-tooltip-trigger-id');
|
console.error('Error destroying tooltip:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
this.log('Running tooltip cleanup');
|
||||||
|
|
||||||
this.pendingAnimationFrames.forEach(id => {
|
this.pendingAnimationFrames.forEach(id => {
|
||||||
cancelAnimationFrame(id);
|
cancelAnimationFrame(id);
|
||||||
});
|
});
|
||||||
this.pendingAnimationFrames.clear();
|
this.pendingAnimationFrames.clear();
|
||||||
|
|
||||||
|
this.pendingTimeouts.forEach(id => {
|
||||||
|
clearTimeout(id);
|
||||||
|
});
|
||||||
|
this.pendingTimeouts.clear();
|
||||||
|
|
||||||
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
|
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
|
||||||
const batchSize = 20;
|
const batchSize = 20;
|
||||||
|
|
||||||
@@ -236,26 +244,144 @@ const TooltipManager = (function() {
|
|||||||
});
|
});
|
||||||
this.pendingAnimationFrames.add(rafId);
|
this.pendingAnimationFrames.add(rafId);
|
||||||
} else {
|
} else {
|
||||||
this.cleanupOrphanedTippyElements();
|
this.cleanupOrphanedTooltips();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (elements.length > 0) {
|
if (elements.length > 0) {
|
||||||
processElementsBatch(0);
|
processElementsBatch(0);
|
||||||
} else {
|
} else {
|
||||||
this.cleanupOrphanedTippyElements();
|
this.cleanupOrphanedTooltips();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltipElementsMap.clear();
|
cleanupOrphanedTooltips() {
|
||||||
}
|
|
||||||
|
|
||||||
cleanupOrphanedTippyElements() {
|
|
||||||
const tippyElements = document.querySelectorAll('[data-tippy-root]');
|
const tippyElements = document.querySelectorAll('[data-tippy-root]');
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
tippyElements.forEach(element => {
|
tippyElements.forEach(element => {
|
||||||
|
const tooltipId = element.getAttribute('data-for-tooltip-id');
|
||||||
|
const trigger = tooltipId ?
|
||||||
|
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (!trigger || !document.body.contains(trigger)) {
|
||||||
if (element.parentNode) {
|
if (element.parentNode) {
|
||||||
element.parentNode.removeChild(element);
|
element.parentNode.removeChild(element);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
this.log(`Removed ${removed} orphaned tooltip elements`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMutationObserver() {
|
||||||
|
if (this.mutationObserver) {
|
||||||
|
this.mutationObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mutationObserver = new MutationObserver(mutations => {
|
||||||
|
let needsCleanup = false;
|
||||||
|
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (mutation.removedNodes.length) {
|
||||||
|
Array.from(mutation.removedNodes).forEach(node => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
|
||||||
|
this.destroy(node);
|
||||||
|
needsCleanup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.querySelectorAll) {
|
||||||
|
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
|
||||||
|
if (tooltipTriggers.length > 0) {
|
||||||
|
tooltipTriggers.forEach(trigger => {
|
||||||
|
this.destroy(trigger);
|
||||||
|
});
|
||||||
|
needsCleanup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needsCleanup) {
|
||||||
|
this.cleanupOrphanedTooltips();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mutationObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.mutationObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
startDisconnectedElementsCheck() {
|
||||||
|
if (this.disconnectedCheckInterval) {
|
||||||
|
clearInterval(this.disconnectedCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disconnectedCheckInterval = setInterval(() => {
|
||||||
|
this.checkForDisconnectedElements();
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForDisconnectedElements() {
|
||||||
|
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
|
||||||
|
let removedCount = 0;
|
||||||
|
|
||||||
|
elements.forEach(element => {
|
||||||
|
if (!document.body.contains(element)) {
|
||||||
|
this.destroy(element);
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removedCount > 0) {
|
||||||
|
this.log(`Removed ${removedCount} tooltips for disconnected elements`);
|
||||||
|
this.cleanupOrphanedTooltips();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startPeriodicCleanup() {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
this.performPeriodicCleanup();
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
|
||||||
|
performPeriodicCleanup(force = false) {
|
||||||
|
this.cleanupOrphanedTooltips();
|
||||||
|
|
||||||
|
this.checkForDisconnectedElements();
|
||||||
|
|
||||||
|
const tooltipCount = document.querySelectorAll('[data-tippy-root]').length;
|
||||||
|
|
||||||
|
if (force || tooltipCount > this.maxTooltips) {
|
||||||
|
this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`);
|
||||||
|
|
||||||
|
this.cleanup();
|
||||||
|
|
||||||
|
if (window.gc) {
|
||||||
|
window.gc();
|
||||||
|
} else {
|
||||||
|
const arr = new Array(1000);
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
arr[i] = new Array(10000).join('x');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupStyles() {
|
setupStyles() {
|
||||||
@@ -350,136 +476,11 @@ const TooltipManager = (function() {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCleanupEvents() {
|
|
||||||
this.boundCleanup = this.cleanup.bind(this);
|
|
||||||
this.handleVisibilityChange = () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
this.cleanup();
|
|
||||||
|
|
||||||
if (window.MemoryManager) {
|
|
||||||
window.MemoryManager.forceCleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', this.boundCleanup);
|
|
||||||
window.addEventListener('unload', this.boundCleanup);
|
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
||||||
|
|
||||||
if (window.CleanupManager) {
|
|
||||||
window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleanupInterval = setInterval(() => {
|
|
||||||
this.performPeriodicCleanup();
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
startDisconnectedElementsCheck() {
|
|
||||||
|
|
||||||
if (this.disconnectedCheckInterval) {
|
|
||||||
clearInterval(this.disconnectedCheckInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.disconnectedCheckInterval = setInterval(() => {
|
|
||||||
this.checkForDisconnectedElements();
|
|
||||||
}, 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkForDisconnectedElements() {
|
|
||||||
if (this.tooltipElementsMap.size === 0) return;
|
|
||||||
|
|
||||||
const elementsToCheck = Array.from(this.tooltipElementsMap.keys());
|
|
||||||
let removedCount = 0;
|
|
||||||
|
|
||||||
elementsToCheck.forEach(element => {
|
|
||||||
|
|
||||||
if (!document.body.contains(element)) {
|
|
||||||
this.destroy(element);
|
|
||||||
removedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (removedCount > 0) {
|
|
||||||
this.cleanupOrphanedTippyElements();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
performPeriodicCleanup() {
|
|
||||||
this.cleanupOrphanedTippyElements();
|
|
||||||
this.checkForDisconnectedElements();
|
|
||||||
|
|
||||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
|
||||||
const sortedTooltips = Array.from(this.tooltipElementsMap.entries())
|
|
||||||
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
||||||
|
|
||||||
const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips);
|
|
||||||
tooltipsToRemove.forEach(([element]) => {
|
|
||||||
this.destroy(element);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeCleanupEvents() {
|
|
||||||
window.removeEventListener('beforeunload', this.boundCleanup);
|
|
||||||
window.removeEventListener('unload', this.boundCleanup);
|
|
||||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
||||||
|
|
||||||
if (this.cleanupInterval) {
|
|
||||||
clearInterval(this.cleanupInterval);
|
|
||||||
this.cleanupInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.disconnectedCheckInterval) {
|
|
||||||
clearInterval(this.disconnectedCheckInterval);
|
|
||||||
this.disconnectedCheckInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeMutationObserver() {
|
|
||||||
if (this.mutationObserver) return;
|
|
||||||
|
|
||||||
this.mutationObserver = new MutationObserver(mutations => {
|
|
||||||
let needsCleanup = false;
|
|
||||||
|
|
||||||
mutations.forEach(mutation => {
|
|
||||||
if (mutation.removedNodes.length) {
|
|
||||||
Array.from(mutation.removedNodes).forEach(node => {
|
|
||||||
if (node.nodeType === 1) {
|
|
||||||
|
|
||||||
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
|
|
||||||
this.destroy(node);
|
|
||||||
needsCleanup = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.querySelectorAll) {
|
|
||||||
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
|
|
||||||
if (tooltipTriggers.length > 0) {
|
|
||||||
tooltipTriggers.forEach(el => {
|
|
||||||
this.destroy(el);
|
|
||||||
});
|
|
||||||
needsCleanup = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (needsCleanup) {
|
|
||||||
this.cleanupOrphanedTippyElements();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mutationObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeTooltips(selector = '[data-tooltip-target]') {
|
initializeTooltips(selector = '[data-tooltip-target]') {
|
||||||
document.querySelectorAll(selector).forEach(element => {
|
document.querySelectorAll(selector).forEach(element => {
|
||||||
const targetId = element.getAttribute('data-tooltip-target');
|
const targetId = element.getAttribute('data-tooltip-target');
|
||||||
|
if (!targetId) return;
|
||||||
|
|
||||||
const tooltipContent = document.getElementById(targetId);
|
const tooltipContent = document.getElementById(targetId);
|
||||||
|
|
||||||
if (tooltipContent) {
|
if (tooltipContent) {
|
||||||
@@ -491,6 +492,8 @@ const TooltipManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
this.log('Disposing TooltipManager');
|
||||||
|
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
this.pendingAnimationFrames.forEach(id => {
|
this.pendingAnimationFrames.forEach(id => {
|
||||||
@@ -498,31 +501,50 @@ const TooltipManager = (function() {
|
|||||||
});
|
});
|
||||||
this.pendingAnimationFrames.clear();
|
this.pendingAnimationFrames.clear();
|
||||||
|
|
||||||
|
this.pendingTimeouts.forEach(id => {
|
||||||
|
clearTimeout(id);
|
||||||
|
});
|
||||||
|
this.pendingTimeouts.clear();
|
||||||
|
|
||||||
|
if (this.disconnectedCheckInterval) {
|
||||||
|
clearInterval(this.disconnectedCheckInterval);
|
||||||
|
this.disconnectedCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
this.cleanupInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.mutationObserver) {
|
if (this.mutationObserver) {
|
||||||
this.mutationObserver.disconnect();
|
this.mutationObserver.disconnect();
|
||||||
this.mutationObserver = null;
|
this.mutationObserver = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeCleanupEvents();
|
|
||||||
|
|
||||||
const styleElement = document.getElementById('tooltip-styles');
|
const styleElement = document.getElementById('tooltip-styles');
|
||||||
if (styleElement && styleElement.parentNode) {
|
if (styleElement && styleElement.parentNode) {
|
||||||
styleElement.parentNode.removeChild(styleElement);
|
styleElement.parentNode.removeChild(styleElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeTooltips = new WeakMap();
|
|
||||||
this.tooltipElementsMap.clear();
|
|
||||||
|
|
||||||
instance = null;
|
instance = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDebugMode(enabled) {
|
||||||
|
this.debug = Boolean(enabled);
|
||||||
|
return this.debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(options = {}) {
|
initialize(options = {}) {
|
||||||
|
|
||||||
if (options.maxTooltips) {
|
if (options.maxTooltips) {
|
||||||
this.maxTooltips = options.maxTooltips;
|
this.maxTooltips = options.maxTooltips;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('TooltipManager initialized');
|
if (options.debug !== undefined) {
|
||||||
|
this.setDebugMode(options.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('TooltipManager initialized');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,7 +560,7 @@ const TooltipManager = (function() {
|
|||||||
|
|
||||||
getInstance: function() {
|
getInstance: function() {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
const manager = new TooltipManagerImpl();
|
this.initialize();
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
@@ -563,6 +585,11 @@ const TooltipManager = (function() {
|
|||||||
return manager.initializeTooltips(...args);
|
return manager.initializeTooltips(...args);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setDebugMode: function(enabled) {
|
||||||
|
const manager = this.getInstance();
|
||||||
|
return manager.setDebugMode(enabled);
|
||||||
|
},
|
||||||
|
|
||||||
dispose: function(...args) {
|
dispose: function(...args) {
|
||||||
const manager = this.getInstance();
|
const manager = this.getInstance();
|
||||||
return manager.dispose(...args);
|
return manager.dispose(...args);
|
||||||
@@ -570,19 +597,37 @@ const TooltipManager = (function() {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function installTooltipManager() {
|
||||||
|
const originalTooltipManager = window.TooltipManager;
|
||||||
|
|
||||||
window.TooltipManager = TooltipManager;
|
window.TooltipManager = TooltipManager;
|
||||||
|
|
||||||
|
window.TooltipManager.initialize({
|
||||||
|
maxTooltips: 200,
|
||||||
|
debug: false
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (!window.tooltipManagerInitialized) {
|
if (!window.tooltipManagerInitialized) {
|
||||||
TooltipManager.initialize();
|
window.TooltipManager.initializeTooltips();
|
||||||
TooltipManager.initializeTooltips();
|
|
||||||
window.tooltipManagerInitialized = true;
|
window.tooltipManagerInitialized = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return originalTooltipManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
installTooltipManager();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', installTooltipManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
module.exports = TooltipManager;
|
module.exports = TooltipManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager));
|
window.TooltipManager = TooltipManager;
|
||||||
console.log('TooltipManager initialized');
|
console.log('TooltipManager initialized');
|
||||||
|
|||||||
@@ -2343,10 +2343,31 @@ function cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offersBody = document.getElementById('offers-body');
|
||||||
|
if (offersBody) {
|
||||||
|
const existingRows = Array.from(offersBody.querySelectorAll('tr'));
|
||||||
|
existingRows.forEach(row => {
|
||||||
|
const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]');
|
||||||
|
tooltipTriggers.forEach(trigger => {
|
||||||
if (window.TooltipManager) {
|
if (window.TooltipManager) {
|
||||||
if (typeof window.TooltipManager.cleanup === 'function') {
|
window.TooltipManager.destroy(trigger);
|
||||||
window.TooltipManager.cleanup();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.removeListenersByElement(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (row.attributes && row.attributes.length > 0) {
|
||||||
|
row.removeAttribute(row.attributes[0].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (row.firstChild) {
|
||||||
|
row.removeChild(row.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
offersBody.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterForm = document.getElementById('filterForm');
|
const filterForm = document.getElementById('filterForm');
|
||||||
@@ -2358,21 +2379,21 @@ function cleanup() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const paginationButtons = document.querySelectorAll('#prevPage, #nextPage');
|
|
||||||
paginationButtons.forEach(button => {
|
|
||||||
CleanupManager.removeListenersByElement(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
|
|
||||||
CleanupManager.removeListenersByElement(header);
|
|
||||||
});
|
|
||||||
|
|
||||||
cleanupTable();
|
|
||||||
|
|
||||||
jsonData = null;
|
jsonData = null;
|
||||||
originalJsonData = null;
|
originalJsonData = null;
|
||||||
latestPrices = null;
|
latestPrices = null;
|
||||||
|
|
||||||
|
if (window.TooltipManager) {
|
||||||
|
window.TooltipManager.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.MemoryManager) {
|
||||||
|
if (window.MemoryManager.cleanupTooltips) {
|
||||||
|
window.MemoryManager.cleanupTooltips(true);
|
||||||
|
}
|
||||||
|
window.MemoryManager.forceCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Offers.js cleanup completed');
|
console.log('Offers.js cleanup completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during cleanup:', error);
|
console.error('Error during cleanup:', error);
|
||||||
|
|||||||
@@ -120,18 +120,33 @@ const api = {
|
|||||||
fetchCoinGeckoDataXHR: async () => {
|
fetchCoinGeckoDataXHR: async () => {
|
||||||
try {
|
try {
|
||||||
const priceData = await window.PriceManager.getPrices();
|
const priceData = await window.PriceManager.getPrices();
|
||||||
|
|
||||||
const transformedData = {};
|
const transformedData = {};
|
||||||
|
|
||||||
|
const btcPriceUSD = priceData.bitcoin?.usd || 0;
|
||||||
|
if (btcPriceUSD > 0) {
|
||||||
|
window.btcPriceUSD = btcPriceUSD;
|
||||||
|
}
|
||||||
|
|
||||||
window.config.coins.forEach(coin => {
|
window.config.coins.forEach(coin => {
|
||||||
const symbol = coin.symbol.toLowerCase();
|
const symbol = coin.symbol.toLowerCase();
|
||||||
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
|
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
|
||||||
|
|
||||||
if (coinData && coinData.usd) {
|
if (coinData && coinData.usd) {
|
||||||
|
let priceBtc;
|
||||||
|
if (symbol === 'btc') {
|
||||||
|
priceBtc = 1;
|
||||||
|
} else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
|
||||||
|
priceBtc = coinData.usd / window.btcPriceUSD;
|
||||||
|
} else {
|
||||||
|
priceBtc = coinData.btc || 0;
|
||||||
|
}
|
||||||
|
|
||||||
transformedData[symbol] = {
|
transformedData[symbol] = {
|
||||||
current_price: coinData.usd,
|
current_price: coinData.usd,
|
||||||
price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0),
|
price_btc: priceBtc,
|
||||||
displayName: coin.displayName || coin.symbol
|
displayName: coin.displayName || coin.symbol,
|
||||||
|
total_volume: coinData.total_volume,
|
||||||
|
price_change_percentage_24h: coinData.price_change_percentage_24h
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -281,11 +296,9 @@ const ui = {
|
|||||||
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
|
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
|
||||||
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
|
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
|
||||||
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
|
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
|
||||||
|
|
||||||
if (priceUsdElement) {
|
if (priceUsdElement) {
|
||||||
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
|
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (volumeDiv && volumeElement) {
|
if (volumeDiv && volumeElement) {
|
||||||
if (isError || volume24h === null || volume24h === undefined) {
|
if (isError || volume24h === null || volume24h === undefined) {
|
||||||
volumeElement.textContent = 'N/A';
|
volumeElement.textContent = 'N/A';
|
||||||
@@ -294,7 +307,6 @@ const ui = {
|
|||||||
}
|
}
|
||||||
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btcPriceDiv && priceBtcElement) {
|
if (btcPriceDiv && priceBtcElement) {
|
||||||
if (coin === 'BTC') {
|
if (coin === 'BTC') {
|
||||||
btcPriceDiv.style.display = 'none';
|
btcPriceDiv.style.display = 'none';
|
||||||
@@ -303,28 +315,40 @@ const ui = {
|
|||||||
btcPriceDiv.style.display = 'flex';
|
btcPriceDiv.style.display = 'flex';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
|
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data.current_price) {
|
if (!data || !data.current_price) {
|
||||||
throw new Error(`Invalid data structure for ${coin}`);
|
throw new Error(`Invalid data structure for ${coin}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
priceUSD = data.current_price;
|
priceUSD = data.current_price;
|
||||||
priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
|
|
||||||
|
if (coin === 'BTC') {
|
||||||
|
priceBTC = 1;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (data.price_btc !== undefined && data.price_btc !== null) {
|
||||||
|
priceBTC = data.price_btc;
|
||||||
|
}
|
||||||
|
else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
|
||||||
|
priceBTC = priceUSD / window.btcPriceUSD;
|
||||||
|
}
|
||||||
|
else if (app && app.btcPriceUSD && app.btcPriceUSD > 0) {
|
||||||
|
priceBTC = priceUSD / app.btcPriceUSD;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
priceBTC = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
priceChange1d = data.price_change_percentage_24h || 0;
|
priceChange1d = data.price_change_percentage_24h || 0;
|
||||||
volume24h = data.total_volume || 0;
|
volume24h = data.total_volume || 0;
|
||||||
|
|
||||||
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
||||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to display data for ${coin}:`, error.message);
|
logger.error(`Failed to display data for ${coin}:`, error.message);
|
||||||
@@ -1746,7 +1770,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
app.init();
|
app.init();
|
||||||
|
|
||||||
if (window.MemoryManager) {
|
if (window.MemoryManager) {
|
||||||
|
if (typeof MemoryManager.enableAutoCleanup === 'function') {
|
||||||
MemoryManager.enableAutoCleanup();
|
MemoryManager.enableAutoCleanup();
|
||||||
|
} else {
|
||||||
|
MemoryManager.initialize({
|
||||||
|
autoCleanup: true,
|
||||||
|
debug: false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanupManager.setInterval(() => {
|
CleanupManager.setInterval(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user