Complete rewrite of indicators panel to fix duplicate event listeners

Key changes:
- Single 'listenersAttached' flag tracks if any listeners are attached
- One shared event delegation handler handles all button types (add, config, remove, favorite)
- Stop propagation immediately on button clicks to prevent multiple triggers
- Added e.stopPropagation() calls to prevent event bubbling
- Consolidated event listener logic into single function
- Added safety checks before calling window.functionName

This fixes the issue where one click added multiple indicators (3x MA, 6x HTS, 4x RSI) by preventing duplicate event listener setup.
This commit is contained in:
DiTus
2026-02-25 23:04:10 +01:00
parent 43d3a081a3
commit 2c7cbbe073

View File

@ -6,7 +6,7 @@ let configuringId = null;
let searchQuery = '';
let selectedCategory = 'all';
let nextInstanceId = 1;
let eventListenersSet = false;
let listenersAttached = false; // Single flag to track if any listeners are attached
// Chart pane management
let indicatorPanes = new Map();
@ -35,14 +35,6 @@ const CATEGORY_MAP = {
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
// Initialize
export function initIndicatorPanel() {
console.log('[IndicatorPanel] Initializing...');
renderIndicatorPanel();
setupEventListeners();
console.log('[IndicatorPanel] Initialized');
}
function getDefaultColor(index) {
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
}
@ -92,6 +84,12 @@ function groupPlotsByColor(plots) {
return Object.values(groups);
}
export function initIndicatorPanel() {
console.log('[IndicatorPanel] Initializing...');
renderIndicatorPanel();
console.log('[IndicatorPanel] Initialized');
}
export function getActiveIndicators() {
return activeIndicators;
}
@ -136,7 +134,7 @@ export function renderIndicatorPanel() {
value="${searchQuery}"
autocomplete="off"
>
${searchQuery ? `<button class="search-clear" onclick="clearSearch()">×</button>` : ''}
${searchQuery ? `<button class="search-clear">×</button>` : ''}
</div>
<!-- Categories -->
@ -167,7 +165,7 @@ export function renderIndicatorPanel() {
<div class="indicator-section active">
<div class="section-title">
${activeIndicators.length} Active
${activeIndicators.length > 0 ? `<button class="clear-all" onclick="clearAllIndicators()">Clear All</button>` : ''}
${activeIndicators.length > 0 ? `<button class="clear-all">Clear All</button>` : ''}
</div>
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
</div>
@ -188,9 +186,9 @@ export function renderIndicatorPanel() {
`;
// Only setup event listeners once
if (!eventListenersSet) {
if (!listenersAttached) {
setupEventListeners();
eventListenersSet = true;
listenersAttached = true;
}
}
@ -220,19 +218,24 @@ function renderActiveIndicator(indicator) {
const meta = getIndicatorMeta(indicator);
const label = getIndicatorLabel(indicator);
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
const showPresets = meta.name && function() {
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return `<div class="indicator-presets">
<button class="preset-indicator" title="${hasPresets.length} saved presets">💾</button>
</div>`;
}();
return `
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand('${indicator.id}')">
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand && window.toggleIndicatorExpand('${indicator.id}');">
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility && window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
${indicator.visible !== false ? '👁' : '👁‍🗨'}
</button>
<span class="indicator-name">${label}</span>
<div class="indicator-presets">
${meta.name && renderPresetIndicatorIndicator(meta, indicator)}
</div>
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite('${indicator.type}')" title="Add to favorites">
${showPresets}
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite && window.toggleFavorite('${indicator.type}')" title="Add to favorites">
${isFavorite ? '★' : '☆'}
</button>
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
@ -242,7 +245,7 @@ function renderActiveIndicator(indicator) {
${isExpanded ? `
<div class="indicator-config">
${renderIndicatorConfig(indicator, meta)}
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
</div>
` : ''}
</div>
@ -250,10 +253,10 @@ function renderActiveIndicator(indicator) {
}
function renderPresetIndicatorIndicator(meta, indicator) {
const hasPresets = getPresetsForIndicator(meta.name);
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets('${meta.name}')">💾</button>`;
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets && window.showPresets('${meta.name}')">💾</button>`;
}
function renderIndicatorConfig(indicator, meta) {
@ -271,7 +274,7 @@ function renderIndicatorConfig(indicator, meta) {
<div class="config-row">
<label>${group.name} Color</label>
<div class="color-picker">
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}">
<span class="color-preview" style="background: ${color};"></span>
</div>
</div>
@ -280,19 +283,18 @@ function renderIndicatorConfig(indicator, meta) {
<div class="config-row">
<label>Line Type</label>
<select onchange="window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
</select>
</div>
<div class="config-row">
<label>Line Width</label>
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
<span class="range-value">${indicator.params._lineWidth || 2}</span>
</div>
</div>
<!-- Inputs -->
${meta?.inputs && meta.inputs.length > 0 ? `
<div class="config-section">
<div class="section-subtitle">Parameters</div>
@ -300,61 +302,53 @@ function renderIndicatorConfig(indicator, meta) {
<div class="config-row">
<label>${input.label}</label>
${input.type === 'select' ?
`<select onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
`<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
</select>` :
`<div class="input-with-preset">
<input
`<input
type="number"
value="${indicator.params[input.name]}"
${input.min !== undefined ? `min="${input.min}"` : ''}
${input.max !== undefined ? `max="${input.max}"` : ''}
${input.step !== undefined ? `step="${input.step}"` : ''}
onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
>
<button class="presets-btn" onclick="window.showInputPresets('${indicator.id}', '${input.name}')">⋯</button>
</div>`
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
>`
}
</div>
`).join('')}
</div>
` : ''}
<!-- Presets -->
<div class="config-section">
<div class="section-subtitle">
Presets
<button class="preset-action-btn" onclick="window.savePreset('${indicator.id}')" title="Save current settings as preset">+ Save Preset</button>
<button class="preset-action-btn" onclick="window.savePreset && window.savePreset('${indicator.id}')">+ Save Preset</button>
</div>
${renderIndicatorPresets(indicator, meta)}
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
</div>
<!-- Actions -->
<div class="config-actions">
<button class="btn-secondary" onclick="window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
<button class="btn-danger" onclick="window.removeIndicatorById('${indicator.id}')">Remove</button>
<button class="btn-secondary" onclick="window.resetIndicator && window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
<button class="btn-danger" onclick="window.removeIndicator && window.removeIndicator('${indicator.id}')">Remove</button>
</div>
</div>
`;
}
function renderIndicatorPresets(indicator, meta) {
const presets = getPresetsForIndicator(meta.name);
const instance = new IR[indicator.type]({ type: indicator.type, params: indicator.params, name: indicator.name });
const metadata = instance.getMetadata();
const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
return presets.length > 0 ? `
<div class="presets-list">
${presets.map(preset => {
// Match values against current settings
const isApplied = metadata.inputs.every(input =>
preset.values[input.name] === indicator.params[input.name]
${presets.map(p => {
const isApplied = meta.inputs.every(input =>
(indicator.params[input.name] === (preset.values?.[input.name] ?? input.default))
);
return `
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
<span class="preset-label" onclick="window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
<button class="preset-delete" onclick="event.stopPropagation(); window.deletePreset('${preset.id}')" title="Delete preset">×</button>
<span class="preset-label" onclick="window.applyPreset && window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
<button class="preset-delete" onclick="window.deletePreset && window.deletePreset('${preset.id}')">×</button>
</div>
`;
}).join('')}
@ -364,44 +358,59 @@ function renderIndicatorPresets(indicator, meta) {
// Event listeners
function setupEventListeners() {
// Event delegation for dynamically created elements
const container = document.getElementById('indicatorPanel');
if (container) {
// Add button
if (!container) return;
console.log('[IndicatorPanel] Setting up event listeners...');
// Single event delegation handler for add button
container.addEventListener('click', (e) => {
const addBtn = e.target.closest('.indicator-btn.add');
if (addBtn) {
e.stopPropagation();
const type = addBtn.dataset.type;
if (type && window.addIndicator) {
console.log('[IndicatorPanel] Adding indicator:', type);
window.addIndicator(type);
}
return;
}
});
// Config / expand button
container.addEventListener('click', (e) => {
// Expand/collapse button
const expandBtn = e.target.closest('.indicator-btn.expand');
if (expandBtn) {
e.stopPropagation();
const id = expandBtn.dataset.id;
if (id && window.toggleIndicatorExpand) {
window.toggleIndicatorExpand(id);
}
return;
}
});
// Remove button
container.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.indicator-btn.remove');
if (removeBtn) {
e.stopPropagation();
const id = removeBtn.dataset.id;
if (id && window.removeIndicatorById) {
window.removeIndicatorById(id);
}
}
});
return;
}
// Search
// Favorite button
const favoriteBtn = e.target.closest('.indicator-btn.favorite');
if (favoriteBtn) {
e.stopPropagation();
const type = favoriteBtn.dataset.type;
if (type && window.toggleFavorite) {
window.toggleFavorite(type);
}
return;
}
});
// Search input
const searchInput = document.getElementById('indicatorSearch');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
@ -410,6 +419,15 @@ function setupEventListeners() {
});
}
// Search clear button
const searchClear = container.querySelector('.search-clear');
if (searchClear) {
searchClear.addEventListener('click', (e) => {
searchQuery = '';
renderIndicatorPanel();
});
}
// Category tabs
document.querySelectorAll('.category-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
@ -417,6 +435,16 @@ function setupEventListeners() {
renderIndicatorPanel();
});
});
// Clear all button
const clearAllBtn = container.querySelector('.clear-all');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', () => {
window.clearAllIndicators();
});
}
console.log('[IndicatorPanel] Event listeners setup complete');
}
// Actions
@ -469,7 +497,7 @@ function addIndicator(type) {
configuringId = id;
renderIndicatorPanel();
drawIndicatorsOnChart();
};
}
window.toggleIndicatorExpand = function(id) {
configuringId = configuringId === id ? null : id;
@ -492,7 +520,10 @@ window.toggleIndicatorVisibility = function(id) {
};
window.toggleFavorite = function(type) {
const favorites = userPresets.favorites || [];
if (!userPresets) userPresets = {};
if (!userPresets.favorites) userPresets.favorites = [];
const favorites = userPresets.favorites;
const idx = favorites.indexOf(type);
if (idx >= 0) {
@ -536,9 +567,9 @@ window.resetIndicator = function(id) {
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
const metadata = instance.getMetadata();
const meta = instance.getMetadata();
metadata.inputs.forEach(input => {
meta.inputs.forEach(input => {
indicator.params[input.name] = input.default;
});
@ -546,7 +577,12 @@ window.resetIndicator = function(id) {
drawIndicatorsOnChart();
};
function removeIndicatorById(id) {
window.removeIndicator = function() {
if (!configuringId) return;
removeIndicatorById(configuringId);
};
window.removeIndicatorById = function(id) {
const idx = activeIndicators.findIndex(a => a.id === id);
if (idx < 0) return;
@ -562,7 +598,7 @@ function removeIndicatorById(id) {
renderIndicatorPanel();
drawIndicatorsOnChart();
}
};
function removeIndicatorByIndex(index) {
if (index < 0 || index >= activeIndicators.length) return;
@ -571,8 +607,8 @@ function removeIndicatorByIndex(index) {
// Presets
function getPresetsForIndicator(indicatorName) {
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.name);
return allPresets.filter(p => p.indicatorName === indicatorName);
if (!userPresets || !userPresets.presets) return [];
return userPresets.presets.filter(p => p.indicatorName === indicatorName);
}
window.savePreset = function(id) {
@ -608,7 +644,7 @@ window.savePreset = function(id) {
};
window.applyPreset = function(id, presetId) {
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.id);
const allPresets = (userPresets?.presets || []).filter(p => typeof p === 'object' && p.id);
const preset = allPresets.find(p => p.id === presetId);
if (!preset) return;
@ -626,7 +662,7 @@ window.applyPreset = function(id, presetId) {
window.deletePreset = function(presetId) {
if (!confirm('Delete this preset?')) return;
if (userPresets.presets) {
if (userPresets?.presets) {
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
saveUserPresets();
renderIndicatorPanel();
@ -821,6 +857,12 @@ export function drawIndicatorsOnChart() {
});
}
// Export functions for module access
export const addIndicator = addIndicator;
export const removeIndicatorById = removeIndicatorById;
export const removeIndicatorByIndexFunction = removeIndicatorByIndex;
const removeIndicatorByIndex = removeIndicatorByIndex;
// Legacy compatibility functions
window.renderIndicatorList = renderIndicatorPanel;
window.toggleIndicator = addIndicator;
@ -832,20 +874,15 @@ window.showIndicatorConfig = function(id) {
window.applyIndicatorConfig = function() {
// No-op - config is applied immediately
};
window.removeIndicator = function() {
if (!configuringId) return;
removeIndicatorById(configuringId);
};
// Assign to window for backward compatibility
window.toggleIndicator = addIndicator;
window.addIndicator = addIndicator;
window.toggleIndicator = addIndicator;
window.removeIndicatorById = removeIndicatorById;
window.removeIndicatorByIndex = function(index) {
window.removeIndicatorByIndex = removeIndicatorByIndexWindow;
const removeIndicatorByIndexWindow = function(index) {
if (index < 0 || index >= activeIndicators.length) return;
removeIndicatorById(activeIndicators[index].id);
};
window.removeIndicatorByIndex = removeIndicatorByIndexWindow;
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
// Export functions for module imports
export { addIndicator, removeIndicatorById, removeIndicatorByIndex };