mirror of
https://github.com/basicswap/basicswap.git
synced 2025-04-06 14:27:30 +00:00
Merge pull request #274 from gerlofvanek/wsport
Pricechart + Global Tooltips optimization + wsport fix.
This commit is contained in:
commit
7c482bab5c
8 changed files with 729 additions and 271 deletions
basicswap
|
@ -266,18 +266,33 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
connect() {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
try {
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
try {
|
||||
|
||||
let wsPort;
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
|
||||
}
|
||||
},
|
||||
|
||||
if (!wsPort && window.config?.port) {
|
||||
wsPort = window.config.port;
|
||||
}
|
||||
|
||||
if (!wsPort) {
|
||||
wsPort = window.ws_port || '11700';
|
||||
}
|
||||
|
||||
console.log("Using WebSocket port:", wsPort);
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
setupEventHandlers() {
|
||||
this.ws.onopen = () => {
|
||||
state.wsConnected = true;
|
||||
|
|
|
@ -357,17 +357,29 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
connect() {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
try {
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
try {
|
||||
let wsPort;
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
|
||||
}
|
||||
},
|
||||
if (!wsPort && window.config?.port) {
|
||||
wsPort = window.config.port;
|
||||
}
|
||||
if (!wsPort) {
|
||||
wsPort = window.ws_port || '11700';
|
||||
}
|
||||
console.log("Using WebSocket port:", wsPort);
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
setupEventHandlers() {
|
||||
this.ws.onopen = () => {
|
||||
|
|
|
@ -249,21 +249,37 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
connect() {
|
||||
if (this.isConnected() || this.isPaused) return;
|
||||
if (this.isConnected() || this.isPaused) return;
|
||||
|
||||
if (this.ws) {
|
||||
this.cleanupConnection();
|
||||
if (this.ws) {
|
||||
this.cleanupConnection();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let wsPort;
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
|
||||
}
|
||||
|
||||
try {
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
if (!wsPort && window.config?.port) {
|
||||
wsPort = window.config.port;
|
||||
}
|
||||
},
|
||||
|
||||
if (!wsPort) {
|
||||
wsPort = window.ws_port || '11700';
|
||||
}
|
||||
|
||||
console.log("Using WebSocket port:", wsPort);
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
setupEventHandlers() {
|
||||
if (!this.ws) return;
|
||||
|
@ -917,7 +933,6 @@ const forceTooltipDOMCleanup = () => {
|
|||
foundCount += allTooltipElements.length;
|
||||
|
||||
allTooltipElements.forEach(element => {
|
||||
|
||||
const isDetached = !document.body.contains(element) ||
|
||||
element.classList.contains('hidden') ||
|
||||
element.style.display === 'none';
|
||||
|
@ -947,7 +962,6 @@ const forceTooltipDOMCleanup = () => {
|
|||
|
||||
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
|
||||
foundCount += tippyRoots.length;
|
||||
|
||||
tippyRoots.forEach(element => {
|
||||
const isOrphan = !element.children.length ||
|
||||
element.children[0].classList.contains('hidden') ||
|
||||
|
@ -975,13 +989,10 @@ const forceTooltipDOMCleanup = () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle legacy tooltip elements
|
||||
document.querySelectorAll('.tooltip').forEach(element => {
|
||||
const isTrulyDetached = !element.parentElement ||
|
||||
!document.body.contains(element.parentElement) ||
|
||||
element.classList.contains('hidden');
|
||||
|
||||
if (isTrulyDetached) {
|
||||
try {
|
||||
element.remove();
|
||||
|
@ -992,14 +1003,11 @@ const forceTooltipDOMCleanup = () => {
|
|||
}
|
||||
});
|
||||
|
||||
if (window.TooltipManager && window.TooltipManager.activeTooltips) {
|
||||
window.TooltipManager.activeTooltips.forEach((instance, id) => {
|
||||
const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]);
|
||||
const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`);
|
||||
|
||||
if (!tooltipElement || !triggerElement ||
|
||||
!document.body.contains(tooltipElement) ||
|
||||
!document.body.contains(triggerElement)) {
|
||||
if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') {
|
||||
const activeTooltips = window.TooltipManager.getActiveTooltipInstances();
|
||||
activeTooltips.forEach(([element, instance]) => {
|
||||
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!document.body.contains(element)) {
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
|
@ -1007,14 +1015,13 @@ const forceTooltipDOMCleanup = () => {
|
|||
console.warn('Error destroying tooltip instance:', e);
|
||||
}
|
||||
}
|
||||
window.TooltipManager.activeTooltips.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (removedCount > 0) {
|
||||
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const createTableRow = async (bid) => {
|
||||
const identity = await IdentityManager.getIdentityData(bid.addr_from);
|
||||
|
|
|
@ -241,40 +241,47 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
connect() {
|
||||
if (this.connectionState.isConnecting || this.isIntentionallyClosed) {
|
||||
return false;
|
||||
if (this.connectionState.isConnecting || this.isIntentionallyClosed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
this.connectionState.isConnecting = true;
|
||||
this.connectionState.lastConnectAttempt = Date.now();
|
||||
|
||||
try {
|
||||
let wsPort;
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = wsConfig.port || wsConfig.fallbackPort;
|
||||
console.log("Using WebSocket port:", wsPort);
|
||||
} else {
|
||||
wsPort = config?.port || window.ws_port || '11700';
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
this.connectionState.isConnecting = true;
|
||||
this.connectionState.lastConnectAttempt = Date.now();
|
||||
|
||||
try {
|
||||
const wsPort = config.port || window.ws_port || '11700';
|
||||
|
||||
if (!wsPort) {
|
||||
this.connectionState.isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
|
||||
this.connectionState.connectTimeout = setTimeout(() => {
|
||||
if (this.connectionState.isConnecting) {
|
||||
this.cleanup();
|
||||
this.handleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!wsPort) {
|
||||
this.connectionState.isConnecting = false;
|
||||
this.handleReconnect();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
|
||||
this.connectionState.connectTimeout = setTimeout(() => {
|
||||
if (this.connectionState.isConnecting) {
|
||||
this.cleanup();
|
||||
this.handleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.connectionState.isConnecting = false;
|
||||
this.handleReconnect();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
setupEventHandlers() {
|
||||
if (!this.ws) return;
|
||||
|
||||
|
|
|
@ -1,3 +1,128 @@
|
|||
// CLEANUP
|
||||
const cleanupManager = {
|
||||
eventListeners: [],
|
||||
timeouts: [],
|
||||
intervals: [],
|
||||
animationFrames: [],
|
||||
|
||||
addListener: function(element, type, handler, options) {
|
||||
if (!element) return null;
|
||||
element.addEventListener(type, handler, options);
|
||||
this.eventListeners.push({ element, type, handler, options });
|
||||
return handler;
|
||||
},
|
||||
|
||||
setTimeout: function(callback, delay) {
|
||||
const id = setTimeout(callback, delay);
|
||||
this.timeouts.push(id);
|
||||
return id;
|
||||
},
|
||||
|
||||
setInterval: function(callback, delay) {
|
||||
const id = setInterval(callback, delay);
|
||||
this.intervals.push(id);
|
||||
return id;
|
||||
},
|
||||
|
||||
requestAnimationFrame: function(callback) {
|
||||
const id = requestAnimationFrame(callback);
|
||||
this.animationFrames.push(id);
|
||||
return id;
|
||||
},
|
||||
|
||||
clearAll: function() {
|
||||
this.eventListeners.forEach(({ element, type, handler, options }) => {
|
||||
if (element) {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
} catch (e) {
|
||||
console.warn('Error removing event listener:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.eventListeners = [];
|
||||
|
||||
this.timeouts.forEach(id => clearTimeout(id));
|
||||
this.timeouts = [];
|
||||
|
||||
this.intervals.forEach(id => clearInterval(id));
|
||||
this.intervals = [];
|
||||
|
||||
this.animationFrames.forEach(id => cancelAnimationFrame(id));
|
||||
this.animationFrames = [];
|
||||
|
||||
console.log('All resources cleaned up');
|
||||
},
|
||||
|
||||
clearTimeouts: function() {
|
||||
this.timeouts.forEach(id => clearTimeout(id));
|
||||
this.timeouts = [];
|
||||
},
|
||||
|
||||
clearIntervals: function() {
|
||||
this.intervals.forEach(id => clearInterval(id));
|
||||
this.intervals = [];
|
||||
},
|
||||
|
||||
removeListenersByElement: function(element) {
|
||||
if (!element) return;
|
||||
|
||||
const listenersToRemove = this.eventListeners.filter(
|
||||
listener => listener.element === element
|
||||
);
|
||||
|
||||
listenersToRemove.forEach(({ element, type, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
} catch (e) {
|
||||
console.warn('Error removing event listener:', e);
|
||||
}
|
||||
});
|
||||
|
||||
this.eventListeners = this.eventListeners.filter(
|
||||
listener => listener.element !== element
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// MEMORY
|
||||
const memoryMonitor = {
|
||||
isEnabled: true,
|
||||
lastLogTime: 0,
|
||||
logInterval: 5 * 60 * 1000,
|
||||
monitorInterval: null,
|
||||
|
||||
startMonitoring: function() {
|
||||
console.log('Starting memory monitoring');
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
if (this.monitorInterval) {
|
||||
clearInterval(this.monitorInterval);
|
||||
}
|
||||
|
||||
this.monitorInterval = setInterval(() => {
|
||||
this.logMemoryUsage();
|
||||
}, this.logInterval);
|
||||
|
||||
this.logMemoryUsage();
|
||||
},
|
||||
|
||||
logMemoryUsage: function() {
|
||||
console.log('Logging memory usage');
|
||||
if (window.performance && window.performance.memory) {
|
||||
const memory = window.performance.memory;
|
||||
console.log(`Memory Usage: ${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB / ${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB`);
|
||||
}
|
||||
},
|
||||
|
||||
stopMonitoring: function() {
|
||||
if (this.monitorInterval) {
|
||||
clearInterval(this.monitorInterval);
|
||||
this.monitorInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// CONFIG
|
||||
const config = {
|
||||
apiKeys: getAPIKeys(),
|
||||
|
@ -393,49 +518,138 @@ const rateLimiter = {
|
|||
|
||||
// CACHE
|
||||
const cache = {
|
||||
set: (key, value, customTtl = null) => {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
cacheTTL: 5 * 60 * 1000,
|
||||
|
||||
set: function(key, value, customTtl = null) {
|
||||
this.cleanup();
|
||||
|
||||
const item = {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + (customTtl || app.cacheTTL)
|
||||
expiresAt: Date.now() + (customTtl || this.cacheTTL)
|
||||
};
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
//console.log(`Cache set for ${key}, expires in ${(customTtl || app.cacheTTL) / 1000} seconds`);
|
||||
|
||||
try {
|
||||
const serialized = JSON.stringify(item);
|
||||
localStorage.setItem(key, serialized);
|
||||
} catch (e) {
|
||||
console.warn('Cache set error:', e);
|
||||
this.clear();
|
||||
try {
|
||||
const serialized = JSON.stringify(item);
|
||||
localStorage.setItem(key, serialized);
|
||||
} catch (e2) {
|
||||
console.error('Failed to store in cache even after cleanup:', e2);
|
||||
}
|
||||
}
|
||||
},
|
||||
get: (key) => {
|
||||
|
||||
get: function(key) {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
if (!itemStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = JSON.parse(itemStr);
|
||||
const now = Date.now();
|
||||
|
||||
if (now < item.expiresAt) {
|
||||
//console.log(`Cache hit for ${key}, ${(item.expiresAt - now) / 1000} seconds remaining`);
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
} else {
|
||||
//console.log(`Cache expired for ${key}`);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch (error) {
|
||||
//console.error('Error parsing cache item:', error.message);
|
||||
console.error('Error parsing cache item:', error.message);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
isValid: (key) => {
|
||||
return cache.get(key) !== null;
|
||||
|
||||
isValid: function(key) {
|
||||
return this.get(key) !== null;
|
||||
},
|
||||
clear: () => {
|
||||
Object.keys(localStorage).forEach(key => {
|
||||
|
||||
clear: function() {
|
||||
const keysToRemove = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||
localStorage.removeItem(key);
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
//console.log('Cache cleared');
|
||||
|
||||
console.log(`Cache cleared: removed ${keysToRemove.length} items`);
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
let totalSize = 0;
|
||||
const items = [];
|
||||
const keysToRemove = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
const size = new Blob([value]).size;
|
||||
|
||||
const item = JSON.parse(value);
|
||||
|
||||
if (item.expiresAt && item.expiresAt < now) {
|
||||
keysToRemove.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
items.push({
|
||||
key,
|
||||
size,
|
||||
timestamp: item.timestamp || 0,
|
||||
expiresAt: item.expiresAt || 0
|
||||
});
|
||||
} catch (e) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
if (totalSize > this.maxSizeBytes || items.length > this.maxItems) {
|
||||
items.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
const itemsToRemove = Math.max(
|
||||
Math.ceil(items.length * 0.2),
|
||||
items.length - this.maxItems
|
||||
);
|
||||
|
||||
items.slice(0, itemsToRemove).forEach(item => {
|
||||
localStorage.removeItem(item.key);
|
||||
});
|
||||
|
||||
console.log(`Cache cleanup: removed ${itemsToRemove} items, freed ${Math.round((totalSize - this.maxSizeBytes) / 1024)}KB`);
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
itemCount: items.length,
|
||||
removedCount: keysToRemove.length
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -631,6 +845,8 @@ const chartModule = {
|
|||
chart: null,
|
||||
currentCoin: 'BTC',
|
||||
loadStartTime: 0,
|
||||
chartRefs: new WeakMap(),
|
||||
|
||||
verticalLinePlugin: {
|
||||
id: 'verticalLine',
|
||||
beforeDraw: (chart, args, options) => {
|
||||
|
@ -652,15 +868,44 @@ const chartModule = {
|
|||
}
|
||||
},
|
||||
|
||||
initChart: () => {
|
||||
const ctx = document.getElementById('coin-chart')?.getContext('2d');
|
||||
getChartByElement: function(element) {
|
||||
return this.chartRefs.get(element);
|
||||
},
|
||||
|
||||
setChartReference: function(element, chart) {
|
||||
this.chartRefs.set(element, chart);
|
||||
},
|
||||
|
||||
destroyChart: function() {
|
||||
if (chartModule.chart) {
|
||||
try {
|
||||
chartModule.chart.destroy();
|
||||
} catch (e) {
|
||||
console.error('Error destroying chart:', e);
|
||||
}
|
||||
chartModule.chart = null;
|
||||
}
|
||||
},
|
||||
|
||||
initChart: function() {
|
||||
this.destroyChart();
|
||||
|
||||
const canvas = document.getElementById('coin-chart');
|
||||
if (!canvas) {
|
||||
logger.error('Chart canvas element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
logger.error('Failed to get chart context. Make sure the canvas element exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)');
|
||||
gradient.addColorStop(1, 'rgba(77, 132, 240, 0)');
|
||||
|
||||
chartModule.chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
|
@ -811,11 +1056,15 @@ const chartModule = {
|
|||
},
|
||||
plugins: [chartModule.verticalLinePlugin]
|
||||
});
|
||||
|
||||
this.setChartReference(canvas, chartModule.chart);
|
||||
},
|
||||
prepareChartData: (coinSymbol, data) => {
|
||||
|
||||
prepareChartData: function(coinSymbol, data) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
let preparedData;
|
||||
|
||||
|
@ -825,9 +1074,11 @@ const chartModule = {
|
|||
const endUnix = endTime.getTime();
|
||||
const startUnix = endUnix - (24 * 3600000);
|
||||
const hourlyPoints = [];
|
||||
|
||||
for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
|
||||
const targetHour = new Date(hourUnix);
|
||||
targetHour.setUTCMinutes(0, 0, 0);
|
||||
|
||||
const closestPoint = data.reduce((prev, curr) => {
|
||||
const prevTime = new Date(prev[0]);
|
||||
const currTime = new Date(curr[0]);
|
||||
|
@ -868,6 +1119,7 @@ const chartModule = {
|
|||
y: price
|
||||
}));
|
||||
} else {
|
||||
console.warn('Unknown data format for chartData:', data);
|
||||
return [];
|
||||
}
|
||||
return preparedData.map(point => ({
|
||||
|
@ -880,7 +1132,7 @@ const chartModule = {
|
|||
}
|
||||
},
|
||||
|
||||
ensureHourlyData: (data) => {
|
||||
ensureHourlyData: function(data) {
|
||||
const now = new Date();
|
||||
now.setUTCMinutes(0, 0, 0);
|
||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
@ -888,136 +1140,176 @@ const chartModule = {
|
|||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
|
||||
const closestDataPoint = data.reduce((prev, curr) =>
|
||||
Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
|
||||
Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
|
||||
);
|
||||
hourlyData.push({
|
||||
x: targetTime.getTime(),
|
||||
y: closestDataPoint.y
|
||||
});
|
||||
|
||||
if (data.length > 0) {
|
||||
const closestDataPoint = data.reduce((prev, curr) =>
|
||||
Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
|
||||
Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
|
||||
);
|
||||
hourlyData.push({
|
||||
x: targetTime.getTime(),
|
||||
y: closestDataPoint.y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hourlyData;
|
||||
},
|
||||
|
||||
updateChart: async (coinSymbol, forceRefresh = false) => {
|
||||
updateChart: async function(coinSymbol, forceRefresh = false) {
|
||||
try {
|
||||
if (!chartModule.chart) {
|
||||
chartModule.initChart();
|
||||
}
|
||||
const currentChartData = chartModule.chart?.data?.datasets[0]?.data || [];
|
||||
if (currentChartData.length === 0) {
|
||||
chartModule.showChartLoader();
|
||||
}
|
||||
chartModule.loadStartTime = Date.now();
|
||||
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
||||
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
||||
let data;
|
||||
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
||||
data = cachedData.value;
|
||||
} else {
|
||||
try {
|
||||
const currentChartData = chartModule.chart?.data.datasets[0].data || [];
|
||||
if (currentChartData.length === 0) {
|
||||
chartModule.showChartLoader();
|
||||
}
|
||||
chartModule.loadStartTime = Date.now();
|
||||
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
||||
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
||||
let data;
|
||||
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
||||
data = cachedData.value;
|
||||
//console.log(`Using cached data for ${coinSymbol}`);
|
||||
} else {
|
||||
try {
|
||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||
data = allData[coinSymbol];
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw new Error(`No data returned for ${coinSymbol}`);
|
||||
}
|
||||
cache.set(cacheKey, data, config.cacheTTL);
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') && currentChartData.length > 0) {
|
||||
//console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
|
||||
return;
|
||||
}
|
||||
const expiredCache = localStorage.getItem(cacheKey);
|
||||
if (expiredCache) {
|
||||
try {
|
||||
const parsedCache = JSON.parse(expiredCache);
|
||||
data = parsedCache.value;
|
||||
//console.log(`Using expired cache data for ${coinSymbol}`);
|
||||
} catch (cacheError) {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||
if (chartData.length > 0 && chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = chartData;
|
||||
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
||||
if (coinSymbol === 'WOW') {
|
||||
chartModule.chart.options.scales.x.time.unit = 'hour';
|
||||
} else {
|
||||
const resolution = config.resolutions[config.currentResolution];
|
||||
chartModule.chart.options.scales.x.time.unit =
|
||||
resolution.interval === 'hourly' ? 'hour' :
|
||||
config.currentResolution === 'year' ? 'month' : 'day';
|
||||
}
|
||||
chartModule.chart.update('active');
|
||||
chartModule.currentCoin = coinSymbol;
|
||||
const loadTime = Date.now() - chartModule.loadStartTime;
|
||||
ui.updateLoadTimeAndCache(loadTime, cachedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||
if (!(chartModule.chart?.data.datasets[0].data.length > 0)) {
|
||||
chartModule.chart.data.datasets[0].data = [];
|
||||
chartModule.chart.update('active');
|
||||
}
|
||||
} finally {
|
||||
chartModule.hideChartLoader();
|
||||
}
|
||||
},
|
||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||
data = allData[coinSymbol];
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw new Error(`No data returned for ${coinSymbol}`);
|
||||
}
|
||||
|
||||
showChartLoader: () => {
|
||||
cache.set(cacheKey, data, config.cacheTTL);
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') && currentChartData.length > 0) {
|
||||
console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
|
||||
chartModule.hideChartLoader();
|
||||
return;
|
||||
}
|
||||
const expiredCache = localStorage.getItem(cacheKey);
|
||||
if (expiredCache) {
|
||||
try {
|
||||
const parsedCache = JSON.parse(expiredCache);
|
||||
data = parsedCache.value;
|
||||
} catch (cacheError) {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chartModule.currentCoin !== coinSymbol) {
|
||||
chartModule.destroyChart();
|
||||
chartModule.initChart();
|
||||
}
|
||||
|
||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||
if (chartData.length > 0 && chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = chartData;
|
||||
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
||||
if (coinSymbol === 'WOW') {
|
||||
chartModule.chart.options.scales.x.time.unit = 'hour';
|
||||
} else {
|
||||
const resolution = config.resolutions[config.currentResolution];
|
||||
chartModule.chart.options.scales.x.time.unit =
|
||||
resolution.interval === 'hourly' ? 'hour' :
|
||||
config.currentResolution === 'year' ? 'month' : 'day';
|
||||
}
|
||||
chartModule.chart.update('active');
|
||||
chartModule.currentCoin = coinSymbol;
|
||||
const loadTime = Date.now() - chartModule.loadStartTime;
|
||||
ui.updateLoadTimeAndCache(loadTime, cachedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||
|
||||
// Keep existing chart data if possible /todo
|
||||
if (!(chartModule.chart?.data?.datasets[0]?.data?.length > 0)) {
|
||||
if (!chartModule.chart) {
|
||||
chartModule.initChart();
|
||||
}
|
||||
if (chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = [];
|
||||
chartModule.chart.update('active');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chartModule.hideChartLoader();
|
||||
}
|
||||
},
|
||||
|
||||
showChartLoader: function() {
|
||||
const loader = document.getElementById('chart-loader');
|
||||
const chart = document.getElementById('coin-chart');
|
||||
if (!loader || !chart) {
|
||||
//console.warn('Chart loader or chart container elements not found');
|
||||
return;
|
||||
}
|
||||
loader.classList.remove('hidden');
|
||||
chart.classList.add('hidden');
|
||||
},
|
||||
|
||||
hideChartLoader: () => {
|
||||
hideChartLoader: function() {
|
||||
const loader = document.getElementById('chart-loader');
|
||||
const chart = document.getElementById('coin-chart');
|
||||
if (!loader || !chart) {
|
||||
//console.warn('Chart loader or chart container elements not found');
|
||||
return;
|
||||
}
|
||||
loader.classList.add('hidden');
|
||||
chart.classList.remove('hidden');
|
||||
},
|
||||
cleanup: function() {
|
||||
this.destroyChart();
|
||||
this.currentCoin = null;
|
||||
this.loadStartTime = 0;
|
||||
console.log('Chart module cleaned up');
|
||||
}
|
||||
};
|
||||
|
||||
Chart.register(chartModule.verticalLinePlugin);
|
||||
|
||||
const volumeToggle = {
|
||||
isVisible: localStorage.getItem('volumeToggleState') === 'true',
|
||||
init: () => {
|
||||
const toggleButton = document.getElementById('toggle-volume');
|
||||
if (toggleButton) {
|
||||
const volumeToggle = {
|
||||
isVisible: localStorage.getItem('volumeToggleState') === 'true',
|
||||
init: function() {
|
||||
const toggleButton = document.getElementById('toggle-volume');
|
||||
if (toggleButton) {
|
||||
if (typeof cleanupManager !== 'undefined') {
|
||||
cleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle);
|
||||
} else {
|
||||
toggleButton.addEventListener('click', volumeToggle.toggle);
|
||||
volumeToggle.updateVolumeDisplay();
|
||||
}
|
||||
},
|
||||
toggle: () => {
|
||||
volumeToggle.isVisible = !volumeToggle.isVisible;
|
||||
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
|
||||
volumeToggle.updateVolumeDisplay();
|
||||
},
|
||||
updateVolumeDisplay: () => {
|
||||
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
|
||||
volumeDivs.forEach(div => {
|
||||
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||
});
|
||||
const toggleButton = document.getElementById('toggle-volume');
|
||||
if (toggleButton) {
|
||||
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
toggle: function() {
|
||||
volumeToggle.isVisible = !volumeToggle.isVisible;
|
||||
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
|
||||
volumeToggle.updateVolumeDisplay();
|
||||
},
|
||||
|
||||
updateVolumeDisplay: function() {
|
||||
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
|
||||
volumeDivs.forEach(div => {
|
||||
if (div) {
|
||||
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const toggleButton = document.getElementById('toggle-volume');
|
||||
if (toggleButton) {
|
||||
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
const toggleButton = document.getElementById('toggle-volume');
|
||||
if (toggleButton) {
|
||||
toggleButton.removeEventListener('click', volumeToggle.toggle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updateButtonStyles(button, isActive, color) {
|
||||
button.classList.toggle('text-' + color + '-500', isActive);
|
||||
|
@ -1042,7 +1334,7 @@ const app = {
|
|||
minimumRefreshInterval: 60 * 1000, // 1 min
|
||||
|
||||
init: () => {
|
||||
//console.log('Init');
|
||||
console.log('Init');
|
||||
window.addEventListener('load', app.onLoad);
|
||||
app.loadLastRefreshedTime();
|
||||
app.updateAutoRefreshButton();
|
||||
|
@ -1689,5 +1981,48 @@ resolutionButtons.forEach(button => {
|
|||
});
|
||||
});
|
||||
|
||||
// LOAD
|
||||
const appCleanup = {
|
||||
init: function() {
|
||||
memoryMonitor.startMonitoring();
|
||||
window.addEventListener('beforeunload', this.globalCleanup);
|
||||
},
|
||||
|
||||
globalCleanup: function() {
|
||||
try {
|
||||
if (app.autoRefreshInterval) {
|
||||
clearTimeout(app.autoRefreshInterval);
|
||||
}
|
||||
if (chartModule) {
|
||||
chartModule.cleanup();
|
||||
}
|
||||
if (volumeToggle) {
|
||||
volumeToggle.cleanup();
|
||||
}
|
||||
cleanupManager.clearAll();
|
||||
memoryMonitor.stopMonitoring();
|
||||
cache.clear();
|
||||
|
||||
console.log('Global application cleanup completed');
|
||||
} catch (error) {
|
||||
console.error('Error during global cleanup:', error);
|
||||
}
|
||||
},
|
||||
manualCleanup: function() {
|
||||
this.globalCleanup();
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
app.init = () => {
|
||||
//console.log('Init');
|
||||
window.addEventListener('load', app.onLoad);
|
||||
appCleanup.init();
|
||||
app.loadLastRefreshedTime();
|
||||
app.updateAutoRefreshButton();
|
||||
memoryMonitor.startMonitoring();
|
||||
//console.log('App initialized');
|
||||
};
|
||||
|
||||
// LOAD
|
||||
app.init();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
class TooltipManager {
|
||||
constructor() {
|
||||
this.activeTooltips = new Map();
|
||||
this.sizeCheckIntervals = new Map();
|
||||
this.activeTooltips = new WeakMap();
|
||||
this.sizeCheckIntervals = new WeakMap();
|
||||
this.tooltipIdCounter = 0;
|
||||
this.setupStyles();
|
||||
this.setupCleanupEvents();
|
||||
this.initializeMutationObserver();
|
||||
}
|
||||
|
||||
static initialize() {
|
||||
|
@ -19,16 +21,26 @@ class TooltipManager {
|
|||
this.destroy(element);
|
||||
|
||||
const checkSize = () => {
|
||||
if (!document.body.contains(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width && rect.height) {
|
||||
clearInterval(this.sizeCheckIntervals.get(element));
|
||||
this.sizeCheckIntervals.delete(element);
|
||||
delete element._tooltipRetryCount;
|
||||
this.createTooltip(element, content, options, rect);
|
||||
} else {
|
||||
const retryCount = element._tooltipRetryCount || 0;
|
||||
if (retryCount < 5) {
|
||||
element._tooltipRetryCount = retryCount + 1;
|
||||
requestAnimationFrame(checkSize);
|
||||
} else {
|
||||
delete element._tooltipRetryCount;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50));
|
||||
checkSize();
|
||||
requestAnimationFrame(checkSize);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -62,6 +74,8 @@ class TooltipManager {
|
|||
}
|
||||
}
|
||||
|
||||
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
|
||||
|
||||
const instance = tippy(element, {
|
||||
content,
|
||||
allowHTML: true,
|
||||
|
@ -75,6 +89,28 @@ class TooltipManager {
|
|||
theme: '',
|
||||
moveTransition: 'none',
|
||||
offset: [0, 10],
|
||||
onShow(instance) {
|
||||
if (!document.body.contains(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onMount(instance) {
|
||||
if (instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
|
||||
}
|
||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||
if (arrow) {
|
||||
arrow.style.setProperty('color', arrowColor, 'important');
|
||||
}
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
|
@ -93,45 +129,11 @@ class TooltipManager {
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
onCreate(instance) {
|
||||
instance._originalPlacement = instance.props.placement;
|
||||
},
|
||||
onShow(instance) {
|
||||
if (!document.body.contains(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
instance.setProps({
|
||||
placement: instance._originalPlacement
|
||||
});
|
||||
|
||||
if (instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onMount(instance) {
|
||||
if (instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
}
|
||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||
if (arrow) {
|
||||
arrow.style.setProperty('color', arrowColor, 'important');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id') ||
|
||||
`tooltip-${Math.random().toString(36).substring(7)}`;
|
||||
element.setAttribute('data-tooltip-trigger-id', id);
|
||||
this.activeTooltips.set(id, instance);
|
||||
element.setAttribute('data-tooltip-trigger-id', tooltipId);
|
||||
this.activeTooltips.set(element, instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
@ -139,40 +141,33 @@ class TooltipManager {
|
|||
destroy(element) {
|
||||
if (!element) return;
|
||||
|
||||
if (this.sizeCheckIntervals.has(element)) {
|
||||
clearInterval(this.sizeCheckIntervals.get(element));
|
||||
this.sizeCheckIntervals.delete(element);
|
||||
}
|
||||
|
||||
delete element._tooltipRetryCount;
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!id) return;
|
||||
|
||||
const instance = this.activeTooltips.get(id);
|
||||
const instance = this.activeTooltips.get(element);
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying tooltip:', e);
|
||||
|
||||
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
|
||||
if (tippyRoot && tippyRoot.parentNode) {
|
||||
tippyRoot.parentNode.removeChild(tippyRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.activeTooltips.delete(id);
|
||||
|
||||
this.activeTooltips.delete(element);
|
||||
element.removeAttribute('data-tooltip-trigger-id');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval));
|
||||
this.sizeCheckIntervals.clear();
|
||||
|
||||
this.activeTooltips.forEach((instance, id) => {
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error cleaning up tooltip:', e);
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
||||
this.destroy(element);
|
||||
});
|
||||
this.activeTooltips.clear();
|
||||
|
||||
document.querySelectorAll('[data-tippy-root]').forEach(element => {
|
||||
if (element.parentNode) {
|
||||
|
@ -181,6 +176,63 @@ class TooltipManager {
|
|||
});
|
||||
}
|
||||
|
||||
getActiveTooltipInstances() {
|
||||
const result = [];
|
||||
|
||||
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
||||
const instance = this.activeTooltips.get(element);
|
||||
if (instance) {
|
||||
result.push([element, instance]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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) {
|
||||
node.querySelectorAll('[data-tooltip-trigger-id]').forEach(el => {
|
||||
this.destroy(el);
|
||||
needsCleanup = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (needsCleanup) {
|
||||
document.querySelectorAll('[data-tippy-root]').forEach(element => {
|
||||
const id = element.getAttribute('data-for-tooltip-id');
|
||||
if (id && !document.querySelector(`[data-tooltip-trigger-id="${id}"]`)) {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
setupStyles() {
|
||||
if (document.getElementById('tooltip-styles')) return;
|
||||
|
||||
|
@ -274,13 +326,22 @@ class TooltipManager {
|
|||
}
|
||||
|
||||
setupCleanupEvents() {
|
||||
window.addEventListener('beforeunload', () => this.cleanup());
|
||||
window.addEventListener('unload', () => this.cleanup());
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.boundCleanup = this.cleanup.bind(this);
|
||||
this.handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this.cleanup();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', this.boundCleanup);
|
||||
window.addEventListener('unload', this.boundCleanup);
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
}
|
||||
|
||||
removeCleanupEvents() {
|
||||
window.removeEventListener('beforeunload', this.boundCleanup);
|
||||
window.removeEventListener('unload', this.boundCleanup);
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
}
|
||||
|
||||
initializeTooltips(selector = '[data-tooltip-target]') {
|
||||
|
@ -295,6 +356,26 @@ class TooltipManager {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.cleanup();
|
||||
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
}
|
||||
|
||||
this.removeCleanupEvents();
|
||||
|
||||
const styleElement = document.getElementById('tooltip-styles');
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
|
||||
if (window.TooltipManager === this) {
|
||||
window.TooltipManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
|
|
|
@ -120,6 +120,23 @@
|
|||
updateShutdownButtons();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function getAPIKeys() {
|
||||
return {
|
||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||
coinGecko: "{{ coingecko_api_key|safe }}"
|
||||
};
|
||||
}
|
||||
|
||||
function getWebSocketConfig() {
|
||||
return {
|
||||
port: "{{ ws_port|safe }}",
|
||||
fallbackPort: "11700"
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="dark:bg-gray-700">
|
||||
|
|
|
@ -8,22 +8,6 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function getAPIKeys() {
|
||||
return {
|
||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||
coinGecko: "{{ coingecko_api_key|safe }}"
|
||||
};
|
||||
}
|
||||
|
||||
function getWebSocketConfig() {
|
||||
return {
|
||||
port: "{{ ws_port|safe }}",
|
||||
fallbackPort: "11700"
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="py-3 px-4 mt-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
|
||||
|
|
Loading…
Reference in a new issue