brighter-trading/src/static/indicators.js

1515 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class Indicator_Output {
constructor(name) {
this.legend = {};
}
create_legend(name, chart, lineSeries) {
// Create legend div and append it to the output element
let target_div = document.getElementById('indicator_output');
this.legend[name] = document.createElement('div');
this.legend[name].className = 'legend';
this.legend[name].style.opacity = 0.1; // Initially mostly transparent
this.legend[name].style.transition = 'opacity 1s ease-out'; // Smooth transition for fade-out
target_div.appendChild(this.legend[name]);
this.legend[name].style.display = 'block';
this.legend[name].style.left = 3 + 'px';
this.legend[name].style.top = 3 + 'px';
// subscribe set legend text to crosshair moves
// v5 API: seriesPrices renamed to seriesData, returns full data item
chart.subscribeCrosshairMove((param) => {
const data = param.seriesData.get(lineSeries);
// Extract value from data item (could be {value} or {close} etc)
const priceValue = data ? (data.value !== undefined ? data.value : data.close) : undefined;
this.set_legend_text(priceValue, name);
});
}
set_legend_text(priceValue, name) {
// Ensure the legend for the name exists
if (!this.legend[name]) {
console.warn(`Legend for ${name} not found, skipping legend update.`);
return;
}
// Callback assigned to fire on crosshair movements.
let val = 'n/a';
if (priceValue !== undefined) {
val = (Math.round(priceValue * 100) / 100).toFixed(2);
}
// Update legend text
this.legend[name].innerHTML = `${name} <span style="color:rgba(4, 111, 232, 1)">${val}</span>`;
// Make legend fully visible
this.legend[name].style.opacity = 1;
this.legend[name].style.display = 'block';
// Set a timeout to fade out the legend after 3 seconds
clearTimeout(this.legend[name].fadeTimeout); // Clear any previous timeout to prevent conflicts
this.legend[name].fadeTimeout = setTimeout(() => {
this.legend[name].style.opacity = 0.1; // Gradually fade out
// Set another timeout to hide the element after the fade-out transition
setTimeout(() => {
this.legend[name].style.display = 'none';
}, 1000); // Wait for the fade-out transition to complete (1s)
}, 1000);
}
clear_legend(name) {
// Remove the legend div from the DOM
for (const key in this.legend) {
if (key.startsWith(name)) {
this.legend[key].remove(); // Remove the legend from the DOM
delete this.legend[key]; // Remove the reference from the object
}
}
}
}
iOutput = new Indicator_Output();
// Create a global map to store the mappings
const indicatorMap = new Map();
class Indicator {
constructor(name) {
// The name of the indicator.
this.name = name;
this.lines = [];
this.hist = [];
this.outputs = [];
}
static getIndicatorConfig() {
return {
args: ['name'],
class: this
};
}
init(data) {
console.log(this.name + ': init() unimplemented.');
}
update(data) {
console.log(this.name + ': update() unimplemented.');
}
addHist(name, chart, color = '#26a69a') {
// v5 API: use addSeries with HistogramSeries
this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, {
color: color,
priceFormat: {
type: 'price',
},
priceScaleId: 'volume_ps',
});
// v5: scaleMargins must be set on the price scale, not series options
// Volume should only take up bottom 30% of the chart
chart.priceScale('volume_ps').applyOptions({
scaleMargins: {
top: 0.7,
bottom: 0,
},
});
}
addLine(name, chart, color, lineWidth) {
// v5 API: use addSeries with LineSeries
this.lines[name] = chart.addSeries(LightweightCharts.LineSeries, {
color: color,
lineWidth: lineWidth
});
// Initialise the crosshair legend for the charts with a unique name for each line.
iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]);
}
setLine(lineName, data, value_name) {
let priceValue;
// Check if the data is a multi-value object
if (typeof data === 'object' && data !== null && value_name in data) {
// Multi-value indicator: Extract the array for the specific key
const processedData = data[value_name];
// Set the data for the line
this.lines[lineName].setData(processedData);
// Isolate the last value provided and round to 2 decimal places
priceValue = processedData.at(-1).value;
// Update the display and legend for multi-value indicators
this.updateDisplay(lineName, { [value_name]: priceValue }, 'value');
} else {
// Single-value indicator: Initialize the data directly
this.lines[lineName].setData(data);
// Isolate the last value provided and round to 2 decimal places
priceValue = data.at(-1).value;
// Update the display and legend for single-value indicators
this.updateDisplay(lineName, priceValue, value_name);
}
iOutput.set_legend_text(priceValue, `${this.name}_${lineName}`);
}
updateDisplay(name, priceValue, value_name) {
// Try the old element format first (legacy chart-based display)
let element = document.getElementById(this.name + '_' + value_name);
if (element) {
if (typeof priceValue === 'object' && priceValue !== null) {
// Handle multiple values by joining them into a single string with labels
let currentValues = element.value ? element.value.split(', ').reduce((acc, pair) => {
let [key, val] = pair.split(': ');
if (!isNaN(parseFloat(val))) {
acc[key] = parseFloat(val);
}
return acc;
}, {}) : {};
// Update current values with the new key-value pairs
Object.assign(currentValues, priceValue);
// Set the updated values back to the element
element.value = Object.entries(currentValues)
.filter(([key, value]) => !isNaN(value)) // Skip NaN values
.map(([key, value]) => `${key}: ${(Math.round(value * 100) / 100).toFixed(2)}`)
.join(', '); // Use comma for formatting
} else {
// Handle simple values as before
element.value = (Math.round(priceValue * 100) / 100).toFixed(2);
}
// Adjust the element styling dynamically for wrapping and height
element.style.height = 'auto'; // Reset height
element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content
} else {
// Try the new card-based display element
const cardElement = document.getElementById(`indicator_card_value_${this.name}`);
if (cardElement) {
let displayValue = '--';
if (typeof priceValue === 'object' && priceValue !== null) {
// For object values, get the first numeric value
const values = Object.values(priceValue).filter(v => typeof v === 'number' && !isNaN(v));
if (values.length > 0) {
displayValue = this._formatDisplayValue(values[0]);
}
} else if (typeof priceValue === 'number' && !isNaN(priceValue)) {
displayValue = this._formatDisplayValue(priceValue);
}
cardElement.textContent = displayValue;
}
// Silently ignore if neither element exists (may be during initialization)
}
}
/**
* Formats a numeric value for display in cards
*/
_formatDisplayValue(value) {
if (value === null || value === undefined || isNaN(value)) return '--';
if (Math.abs(value) >= 1000) {
return value.toFixed(0);
} else if (Math.abs(value) >= 100) {
return value.toFixed(1);
} else {
return value.toFixed(2);
}
}
setHist(name, data) {
this.hist[name].setData(data);
}
updateLine(name, data, value_name) {
// Check if the data is a multi-value object
if (typeof data === 'object' && data !== null && value_name in data) {
// Multi-value indicator: Extract the array for the specific key
const processedData = data[value_name];
// Update the line-set data in the chart
this.lines[name].update(processedData);
// Isolate the last value provided and round to 2 decimal places
const priceValue = processedData.at(-1).value;
// Update the display and legend for multi-value indicators
this.updateDisplay(name, { [value_name]: priceValue }, 'value');
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
} else {
// Single-value indicator: Initialize the data directly
this.lines[name].update(data);
// Isolate the last value provided and round to 2 decimal places
const priceValue = data.at(-1).value;
// Update the display and legend for single-value indicators
this.updateDisplay(name, priceValue, value_name);
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
}
}
updateHist(name, data) {
this.hist[name].update(data);
}
removeFromChart(chart) {
// Ensure the chart object is passed
if (!chart) {
console.error("Chart object is missing.");
return;
}
// Remove all line series associated with this indicator
for (let lineName in this.lines) {
if (this.lines[lineName]) {
chart.removeSeries(this.lines[lineName]);
delete this.lines[lineName];
}
}
// Remove all histogram series associated with this indicator
for (let histName in this.hist) {
if (this.hist[histName]) {
chart.removeSeries(this.hist[histName]);
delete this.hist[histName];
}
}
// Remove the legend from the crosshair (if any)
if (iOutput.legend[this.name]) {
iOutput.legend[this.name].remove();
delete iOutput.legend[this.name];
}
}
}
class SMA extends Indicator {
constructor(name, chart, color, lineWidth = 2) {
super(name);
this.addLine('line', chart, color, lineWidth);
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'chart_1', 'color'],
class: this
};
}
init(data) {
this.setLine('line', data, 'value');
}
update(data) {
this.updateLine('line', data[0], 'value');
}
}
// Register SMA in the map
indicatorMap.set("SMA", SMA);
class Linear_Regression extends SMA {
// Inherits getIndicatorConfig from SMA
}
indicatorMap.set("LREG", Linear_Regression);
class EMA extends SMA {
// Inherits getIndicatorConfig from SMA
}
indicatorMap.set("EMA", EMA);
class RSI extends Indicator {
constructor(name, charts, color, lineWidth = 2) {
super(name);
if (!charts.hasOwnProperty('chart2')) {
charts.create_RSI_chart();
}
let chart = charts.chart2;
this.addLine('line', chart, color, lineWidth);
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'charts', 'color'],
class: this
};
}
init(data) {
this.setLine('line', data, 'value');
}
update(data) {
this.updateLine('line', data[0], 'value');
}
}
indicatorMap.set("RSI", RSI);
class BollingerPercentB extends Indicator {
constructor(name, charts, color, lineWidth = 2) {
super(name);
if (!charts.hasOwnProperty('chart4')) {
charts.create_PercentB_chart();
}
let chart = charts.chart4;
this.addLine('line', chart, color, lineWidth);
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'charts', 'color'],
class: this
};
}
init(data) {
this.setLine('line', data, 'value');
}
update(data) {
this.updateLine('line', data[0], 'value');
}
}
indicatorMap.set("BOL%B", BollingerPercentB);
class MACD extends Indicator {
constructor(name, charts, color_1, color_2, lineWidth = 2) {
super(name);
if (!charts.hasOwnProperty('chart3')) {
charts.create_MACD_chart();
}
let chart = charts.chart3;
this.addLine('line_m', chart, color_1, lineWidth);
this.addLine('line_s', chart, color_2, lineWidth);
this.addHist(name, chart);
this.outputs = ['macd', 'signal', 'hist'];
}
static getIndicatorConfig() {
return {
args: ['name', 'charts', 'color_1', 'color_2'],
class: this
};
}
init(data) {
// Filter out rows where macd, signal, or hist are null
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
if (filteredData.length > 0) {
// Prepare the filtered data for the MACD line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.macd
}));
// Set the 'line_m' for the MACD line
this.setLine('line_m', { macd: line_m }, 'macd');
// Prepare the filtered data for the signal line
const line_s = filteredData.map(row => ({
time: row.time,
value: row.signal
}));
// Set the 'line_s' for the signal line
this.setLine('line_s', { signal: line_s }, 'signal');
// Set the histogram data
this.setHist(this.name, filteredData.map(row => ({
time: row.time,
value: row.hist
})));
} else {
console.error('No valid MACD data found.');
}
}
update(data) {
// Filter out rows where macd, signal, or hist are null
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
if (filteredData.length > 0) {
// Update the 'macd' line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.macd
}));
this.updateLine('line_m', { macd: line_m }, 'macd');
// Update the 'signal' line
const line_s = filteredData.map(row => ({
time: row.time,
value: row.signal
}));
this.updateLine('line_s', { signal: line_s }, 'signal');
// Update the 'hist' (histogram) bar
const hist_data = filteredData.map(row => ({
time: row.time,
value: row.hist
}));
this.updateHist('hist', hist_data);
} else {
console.error('No valid MACD data found for update.');
}
}
}
indicatorMap.set("MACD", MACD);
class ATR extends Indicator {
// Inherits getIndicatorConfig from Indicator
init(data) {
this.updateDisplay(this.name, data.at(-1).value, 'value');
this.outputs = ['value'];
}
update(data) {
this.updateDisplay(this.name, data[0].value, 'value');
}
}
indicatorMap.set("ATR", ATR);
class Volume extends Indicator {
constructor(name, chart) {
super(name);
this.addHist(name, chart);
this.hist[name].applyOptions({ scaleMargins: { top: 0.95, bottom: 0.0 } });
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'chart_1'],
class: this
};
}
init(data) {
this.setHist(this.name, data);
}
update(data) {
this.updateHist(this.name, data[0]);
}
}
indicatorMap.set("Volume", Volume);
class Bolenger extends Indicator {
constructor(name, chart, color_1, color_2, color_3, lineWidth = 2) {
super(name);
this.addLine('line_u', chart, color_1, lineWidth);
this.addLine('line_m', chart, color_2, lineWidth);
this.addLine('line_l', chart, color_3, lineWidth);
this.outputs = ['upper', 'middle', 'lower'];
}
static getIndicatorConfig() {
return {
args: ['name', 'chart_1', 'color_1', 'color_2', 'color_3'],
class: this
};
}
init(data) {
// Filter out rows where upper, middle, or lower are null
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
if (filteredData.length > 0) {
// Set the 'line_u' for the upper line
const line_u = filteredData.map(row => ({
time: row.time,
value: row.upper
}));
this.setLine('line_u', { upper: line_u }, 'upper');
// Set the 'line_m' for the middle line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.middle
}));
this.setLine('line_m', { middle: line_m }, 'middle');
// Set the 'line_l' for the lower line
const line_l = filteredData.map(row => ({
time: row.time,
value: row.lower
}));
this.setLine('line_l', { lower: line_l }, 'lower');
} else {
console.error('No valid data found for init.');
}
}
update(data) {
// Filter out rows where upper, middle, or lower are null
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
if (filteredData.length > 0) {
// Update the 'upper' line
const line_u = filteredData.map(row => ({
time: row.time,
value: row.upper
}));
this.updateLine('line_u', { upper: line_u }, 'upper');
// Update the 'middle' line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.middle
}));
this.updateLine('line_m', { middle: line_m }, 'middle');
// Update the 'lower' line
const line_l = filteredData.map(row => ({
time: row.time,
value: row.lower
}));
this.updateLine('line_l', { lower: line_l }, 'lower');
} else {
console.error('No valid data found for update.');
}
}
}
indicatorMap.set("BOLBands", Bolenger);
// Candlestick Pattern Indicator class
// Displays pattern signals as histogram bars in a dedicated patterns chart
class CandlestickPattern extends Indicator {
constructor(name, charts, bullish_color, bearish_color) {
super(name);
// Create patterns chart if it doesn't exist
if (!charts.hasOwnProperty('chart5') || !charts.chart5) {
charts.create_patterns_chart();
}
let chart = charts.chart5;
// Store colors for pattern signals (hex format for color picker compatibility)
this.bullish_color = bullish_color || '#00C853';
this.bearish_color = bearish_color || '#FF5252';
// Add histogram series for this pattern
this.addPatternHist(name, chart);
this.outputs = ['value'];
}
static getIndicatorConfig() {
return {
args: ['name', 'charts', 'bullish_color', 'bearish_color'],
class: this
};
}
addPatternHist(name, chart) {
// v5 API: use addSeries with HistogramSeries
this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, {
priceFormat: {
type: 'price',
},
priceScaleId: 'right',
});
// Create legend for the pattern
iOutput.create_legend(`${this.name}_pattern`, chart, this.hist[name]);
}
init(data) {
// Transform data to add colors based on signal value
// Keep ALL data points (including zeros) to maintain proper time scale alignment
// Zero values will have no visible bar (height = 0) but the timestamp is needed
const histData = data.map(d => ({
time: d.time,
value: d.value,
// Use bullish/bearish colors for signals, gray for no signal (won't be visible anyway)
color: d.value > 0 ? this.bullish_color
: d.value < 0 ? this.bearish_color
: '#808080'
}));
this.setHist(this.name, histData);
// Update display with latest signal
const latestValue = data.at(-1).value;
let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None';
this.updateDisplay(this.name, displayText, 'value');
}
update(data) {
// Transform data to add colors based on signal value
// Keep ALL data points to maintain time scale alignment with other charts
const histData = data.map(d => ({
time: d.time,
value: d.value,
color: d.value > 0 ? this.bullish_color
: d.value < 0 ? this.bearish_color
: '#808080'
}));
this.setHist(this.name, histData);
// Update display with latest signal
const latestValue = data.at(-1).value;
let displayText = latestValue > 0 ? 'Bullish' : latestValue < 0 ? 'Bearish' : 'None';
this.updateDisplay(this.name, displayText, 'value');
}
updateDisplay(name, value, value_name) {
// Override to handle text display for patterns
let element = document.getElementById(this.name + '_' + value_name);
if (element) {
element.value = value;
}
}
}
// Register all candlestick pattern types to use the same CandlestickPattern class
// Single candle patterns
indicatorMap.set("CDL_DOJI", CandlestickPattern);
indicatorMap.set("CDL_HAMMER", CandlestickPattern);
indicatorMap.set("CDL_INVERTEDHAMMER", CandlestickPattern);
indicatorMap.set("CDL_SHOOTINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_SPINNINGTOP", CandlestickPattern);
indicatorMap.set("CDL_MARUBOZU", CandlestickPattern);
indicatorMap.set("CDL_DRAGONFLYDOJI", CandlestickPattern);
indicatorMap.set("CDL_GRAVESTONEDOJI", CandlestickPattern);
indicatorMap.set("CDL_HANGINGMAN", CandlestickPattern);
indicatorMap.set("CDL_HIGHWAVE", CandlestickPattern);
indicatorMap.set("CDL_LONGLEGGEDDOJI", CandlestickPattern);
indicatorMap.set("CDL_LONGLINE", CandlestickPattern);
indicatorMap.set("CDL_SHORTLINE", CandlestickPattern);
indicatorMap.set("CDL_RICKSHAWMAN", CandlestickPattern);
indicatorMap.set("CDL_TAKURI", CandlestickPattern);
indicatorMap.set("CDL_BELTHOLD", CandlestickPattern);
indicatorMap.set("CDL_CLOSINGMARUBOZU", CandlestickPattern);
// Two candle patterns
indicatorMap.set("CDL_ENGULFING", CandlestickPattern);
indicatorMap.set("CDL_HARAMI", CandlestickPattern);
indicatorMap.set("CDL_HARAMICROSS", CandlestickPattern);
indicatorMap.set("CDL_PIERCING", CandlestickPattern);
indicatorMap.set("CDL_DARKCLOUDCOVER", CandlestickPattern);
indicatorMap.set("CDL_COUNTERATTACK", CandlestickPattern);
indicatorMap.set("CDL_DOJISTAR", CandlestickPattern);
indicatorMap.set("CDL_HOMINGPIGEON", CandlestickPattern);
indicatorMap.set("CDL_KICKING", CandlestickPattern);
indicatorMap.set("CDL_KICKINGBYLENGTH", CandlestickPattern);
indicatorMap.set("CDL_MATCHINGLOW", CandlestickPattern);
indicatorMap.set("CDL_INNECK", CandlestickPattern);
indicatorMap.set("CDL_ONNECK", CandlestickPattern);
indicatorMap.set("CDL_THRUSTING", CandlestickPattern);
indicatorMap.set("CDL_SEPARATINGLINES", CandlestickPattern);
indicatorMap.set("CDL_STICKSANDWICH", CandlestickPattern);
indicatorMap.set("CDL_GAPSIDESIDEWHITE", CandlestickPattern);
indicatorMap.set("CDL_2CROWS", CandlestickPattern);
// Three+ candle patterns
indicatorMap.set("CDL_MORNINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_EVENINGSTAR", CandlestickPattern);
indicatorMap.set("CDL_MORNINGDOJISTAR", CandlestickPattern);
indicatorMap.set("CDL_EVENINGDOJISTAR", CandlestickPattern);
indicatorMap.set("CDL_3WHITESOLDIERS", CandlestickPattern);
indicatorMap.set("CDL_3BLACKCROWS", CandlestickPattern);
indicatorMap.set("CDL_3INSIDE", CandlestickPattern);
indicatorMap.set("CDL_3OUTSIDE", CandlestickPattern);
indicatorMap.set("CDL_3LINESTRIKE", CandlestickPattern);
indicatorMap.set("CDL_3STARSINSOUTH", CandlestickPattern);
indicatorMap.set("CDL_ABANDONEDBABY", CandlestickPattern);
indicatorMap.set("CDL_ADVANCEBLOCK", CandlestickPattern);
indicatorMap.set("CDL_BREAKAWAY", CandlestickPattern);
indicatorMap.set("CDL_CONCEALBABYSWALL", CandlestickPattern);
indicatorMap.set("CDL_IDENTICAL3CROWS", CandlestickPattern);
indicatorMap.set("CDL_LADDERBOTTOM", CandlestickPattern);
indicatorMap.set("CDL_MATHOLD", CandlestickPattern);
indicatorMap.set("CDL_RISEFALL3METHODS", CandlestickPattern);
indicatorMap.set("CDL_STALLEDPATTERN", CandlestickPattern);
indicatorMap.set("CDL_TASUKIGAP", CandlestickPattern);
indicatorMap.set("CDL_TRISTAR", CandlestickPattern);
indicatorMap.set("CDL_UNIQUE3RIVER", CandlestickPattern);
indicatorMap.set("CDL_UPSIDEGAP2CROWS", CandlestickPattern);
indicatorMap.set("CDL_XSIDEGAP3METHODS", CandlestickPattern);
indicatorMap.set("CDL_HIKKAKE", CandlestickPattern);
indicatorMap.set("CDL_HIKKAKEMOD", CandlestickPattern);
class Indicators {
constructor(comms) {
// Contains instantiated indicators.
this.i_objs = {};
this.comms = comms;
// Store indicator configurations for editing
this.indicators = {};
// Store indicator data (values) for card display
this.indicator_data = {};
}
create_indicators(indicators, charts) {
for (let name in indicators) {
if (!indicators[name].visible) continue;
let i_type = indicators[name].type;
let IndicatorClass = indicatorMap.get(i_type);
if (IndicatorClass) {
let { args, class: IndicatorConstructor } = IndicatorClass.getIndicatorConfig();
let preparedArgs = args.map(arg => {
if (arg === 'name') return name;
if (arg === 'charts') return charts;
if (arg === 'chart_1') return charts.chart_1;
if (arg === 'color') return indicators[name].color;
if (arg === 'color_1') return indicators[name].color_1 || 'red';
if (arg === 'color_2') return indicators[name].color_2 || 'white';
if (arg === 'color_3') return indicators[name].color_3 || 'blue';
if (arg === 'bullish_color') return indicators[name].bullish_color || '#00C853';
if (arg === 'bearish_color') return indicators[name].bearish_color || '#FF5252';
});
this.i_objs[name] = new IndicatorConstructor(...preparedArgs);
} else {
console.error(`Unknown indicator type: ${i_type}`);
}
}
}
// Method to retrieve outputs for each indicator
getIndicatorOutputs() {
let indicatorOutputs = {};
for (let name in this.i_objs) {
if (this.i_objs[name].outputs) {
indicatorOutputs[name] = this.i_objs[name].outputs;
} else {
indicatorOutputs[name] = ['value']; // Default output if not defined
}
}
// Include external indicators (historical API data)
const externalIndicators = window.UI?.signals?.externalIndicators || [];
for (const extInd of externalIndicators) {
// External indicators have a single 'value' output
indicatorOutputs[extInd.name] = ['value'];
}
return indicatorOutputs;
}
addToCharts(charts, idata){
/*
Receives indicator data, creates and stores the indicator
objects, then inserts the data into the charts.
*/
// Always set up the visibility form event handler
this._setupIndicatorForm();
// Store indicator configurations for card rendering and editing
if (idata.indicators) {
this.indicators = idata.indicators;
}
// Store indicator data (values) for card display
if (idata.indicator_data) {
this.indicator_data = idata.indicator_data;
}
if (idata.indicators && Object.keys(idata.indicators).length > 0) {
this.create_indicators(idata.indicators, charts);
// Initialize each indicator with the data directly
if (idata.indicator_data && Object.keys(idata.indicator_data).length > 0) {
this.init_indicators(idata.indicator_data);
} else {
console.warn('Indicator data is empty. No indicators to initialize.');
}
} else {
console.log('No indicators defined for this user.');
}
// Render indicator cards in the UI (after init so values are available)
this.renderIndicators();
}
_setupIndicatorForm(){
// Set up the visibility form event handler
const form = document.getElementById('indicator-form');
if (form && !form._hasSubmitListener) {
form.addEventListener('submit', this.handleFormSubmit.bind(this));
form._hasSubmitListener = true; // Prevent duplicate listeners
}
}
init_indicators(data){
// Loop through all the indicators.
for (name in data){
// Call the initialization function for each indicator.
if (!this.i_objs[name]){
console.log('could not load:', name);
continue;
}
this.i_objs[name].init(data[name]);
}
}
// This updates all the indicator data (re-initializes with full dataset)
update(updates){
for (let name in updates){
if (window.UI.indicators.i_objs[name]) {
// Use init() to refresh with full data on candle close
window.UI.indicators.i_objs[name].init(updates[name]);
// Update stored data and card display
this.indicator_data[name] = updates[name];
this._updateCardValue(name, updates[name]);
} else {
console.warn(`Indicator "${name}" not found in i_objs, skipping update`);
}
}
}
/**
* Updates a single indicator card's displayed value
*/
_updateCardValue(name, data) {
const valueEl = document.getElementById(`indicator_card_value_${name}`);
if (!valueEl) return;
let displayValue = '--';
if (data && Array.isArray(data) && data.length > 0) {
const lastPoint = data[data.length - 1];
if (lastPoint.value !== undefined && lastPoint.value !== null) {
displayValue = this._formatValue(lastPoint.value);
} else if (lastPoint.macd !== undefined) {
displayValue = this._formatValue(lastPoint.macd);
} else if (lastPoint.middle !== undefined) {
displayValue = this._formatValue(lastPoint.middle);
} else if (lastPoint.upper !== undefined) {
displayValue = this._formatValue(lastPoint.upper);
}
}
valueEl.textContent = displayValue;
}
deleteIndicator(indicator, event) {
this.comms.deleteIndicator(indicator).then(response => {
if (response.success) {
const indicatorElement = event.target.closest('.indicator-row');
indicatorElement.remove(); // Remove from DOM
// Remove the indicator from the chart and legend
if (this.i_objs[indicator]) {
let chart;
// Determine which chart the indicator is on, based on its type
const indicatorType = this.i_objs[indicator].constructor.name;
if (indicator.includes('RSI')) {
chart = window.UI.charts.chart2; // Assume RSI is on chart2
} else if (indicator.includes('MACD')) {
chart = window.UI.charts.chart3; // Assume MACD is on chart3
} else if (indicator.includes('%B') || indicator.includes('PercentB')) {
chart = window.UI.charts.chart4; // %B is on chart4
} else if (indicatorType === 'CandlestickPattern') {
chart = window.UI.charts.chart5; // Candlestick patterns on chart5
} else {
chart = window.UI.charts.chart_1; // Default to the main chart
}
// Pass the correct chart object when removing the indicator
this.i_objs[indicator].removeFromChart(chart);
// Remove the indicator object from i_objs
delete this.i_objs[indicator];
// Optionally: Clear the legend entry
iOutput.clear_legend(indicator);
}
} else {
alert('Failed to delete the indicator.');
}
});
}
// This updates a specific indicator
updateIndicator(event) {
event.preventDefault(); // Prevent default form submission behavior
const row = event.target.closest('.indicator-row');
const inputs = row.querySelectorAll('input, select');
// Gather the indicator name from the row
const nameDiv = row.querySelector('div:nth-child(2)'); // Second <div> contains the name
const indicatorName = nameDiv.innerText.trim(); // Get the indicator name
// Initialize formObj with the name of the indicator
const formObj = {
name: indicatorName,
visible: false, // Default value for visible (will be updated based on the checkbox input)
source: {}, // Initialize the source object directly at the top level
properties: {} // Initialize the properties object
};
// Define an exclusion list for properties that should not be parsed as numbers
const exclusionFields = ['custom_field_name']; // Add field names that should NOT be parsed as numbers
// Function to check if a value contains mixed data (e.g., 'abc123')
const isMixedData = (value) => /\D/.test(value) && /\d/.test(value);
// Iterate over each input (text, checkbox, select) and add its name and value to formObj
inputs.forEach(input => {
let value = input.value;
// Handle the 'visible' checkbox separately
if (input.name === 'visible') {
formObj.visible = input.checked;
} else if (['market', 'timeframe', 'exchange'].includes(input.name)) {
// Directly map inputs to source object fields
formObj.source[input.name] = input.value;
} else {
// Check if the value should be parsed as a number
if (!exclusionFields.includes(input.name) && !isMixedData(value)) {
const parsedValue = parseFloat(value);
value = isNaN(parsedValue) ? value : parsedValue;
} else if (input.type === 'checkbox') {
value = input.checked;
}
// Add the processed value to the properties object
formObj.properties[input.name] = value;
}
});
// Call comms to send data to the server
this.comms.updateIndicator(formObj).then(response => {
if (response.success) {
window.location.reload(); // This triggers a full page refresh
} else {
alert('Failed to update the indicator.');
}
}).catch(error => {
console.error('Error updating indicator:', error);
alert('An unexpected error occurred while updating the indicator.');
});
}
add_to_list(){
// Adds user input to a list and displays it in a HTML element.
// called from html button click add property
// Collect the property name and value input by the user
let n = document.getElementById("new_prop_name").value.trim();
let v = document.getElementById("new_prop_value").value.trim();
// Ensure the property name and value are not empty
if (!n || !v) {
alert("Property name and value are required.");
return;
}
// Converts css color name to hex
if (n === 'color'){
// list of valid css colors
let colours = {
"aliceblue":"#f0f8ff", "antiquewhite":"#faebd7", "aqua":"#00ffff", "aquamarine":"#7fffd4", "azure":"#f0ffff", "beige":"#f5f5dc", "bisque":"#ffe4c4", "black":"#000000", "blanchedalmond":"#ffebcd", "blue":"#0000ff", "blueviolet":"#8a2be2", "brown":"#a52a2a", "burlywood":"#deb887", "cadetblue":"#5f9ea0", "chartreuse":"#7fff00", "chocolate":"#d2691e", "coral":"#ff7f50", "cornflowerblue":"#6495ed", "cornsilk":"#fff8dc", "crimson":"#dc143c", "cyan":"#00ffff", "darkblue":"#00008b", "darkcyan":"#008b8b", "darkgoldenrod":"#b8860b", "darkgray":"#a9a9a9", "darkgreen":"#006400", "darkkhaki":"#bdb76b", "darkmagenta":"#8b008b", "darkolivegreen":"#556b2f", "darkorange":"#ff8c00", "darkorchid":"#9932cc", "darkred":"#8b0000", "darksalmon":"#e9967a", "darkseagreen":"#8fbc8f", "darkslateblue":"#483d8b", "darkslategray":"#2f4f4f", "darkturquoise":"#00ced1", "darkviolet":"#9400d3", "deeppink":"#ff1493", "deepskyblue":"#00bfff", "dimgray":"#696969", "dodgerblue":"#1e90ff", "firebrick":"#b22222", "floralwhite":"#fffaf0", "forestgreen":"#228b22", "fuchsia":"#ff00ff", "gainsboro":"#dcdcdc", "ghostwhite":"#f8f8ff", "gold":"#ffd700", "goldenrod":"#daa520", "gray":"#808080", "green":"#008000", "greenyellow":"#adff2f",
"honeydew":"#f0fff0", "hotpink":"#ff69b4", "indianred ":"#cd5c5c", "indigo":"#4b0082", "ivory":"#fffff0", "khaki":"#f0e68c", "lavender":"#e6e6fa", "lavenderblush":"#fff0f5", "lawngreen":"#7cfc00", "lemonchiffon":"#fffacd", "lightblue":"#add8e6", "lightcoral":"#f08080", "lightcyan":"#e0ffff", "lightgoldenrodyellow":"#fafad2", "lightgrey":"#d3d3d3", "lightgreen":"#90ee90", "lightpink":"#ffb6c1", "lightsalmon":"#ffa07a", "lightseagreen":"#20b2aa", "lightskyblue":"#87cefa", "lightslategray":"#778899", "lightsteelblue":"#b0c4de", "lightyellow":"#ffffe0", "lime":"#00ff00", "limegreen":"#32cd32", "linen":"#faf0e6", "magenta":"#ff00ff", "maroon":"#800000", "mediumaquamarine":"#66cdaa", "mediumblue":"#0000cd", "mediumorchid":"#ba55d3", "mediumpurple":"#9370d8", "mediumseagreen":"#3cb371", "mediumslateblue":"#7b68ee", "mediumspringgreen":"#00fa9a", "mediumturquoise":"#48d1cc", "mediumvioletred":"#c71585", "midnightblue":"#191970", "mintcream":"#f5fffa", "mistyrose":"#ffe4e1", "moccasin":"#ffe4b5", "navajowhite":"#ffdead", "navy":"#000080", "oldlace":"#fdf5e6", "olive":"#808000", "olivedrab":"#6b8e23", "orange":"#ffa500", "orangered":"#ff4500", "orchid":"#da70d6", "palegoldenrod":"#eee8aa",
"palegreen":"#98fb98", "paleturquoise":"#afeeee", "palevioletred":"#d87093", "papayawhip":"#ffefd5", "peachpuff":"#ffdab9", "peru":"#cd853f", "pink":"#ffc0cb", "plum":"#dda0dd", "powderblue":"#b0e0e6", "purple":"#800080", "rebeccapurple":"#663399", "red":"#ff0000", "rosybrown":"#bc8f8f", "royalblue":"#4169e1", "saddlebrown":"#8b4513", "salmon":"#fa8072", "sandybrown":"#f4a460", "seagreen":"#2e8b57", "seashell":"#fff5ee", "sienna":"#a0522d", "silver":"#c0c0c0", "skyblue":"#87ceeb", "slateblue":"#6a5acd", "slategray":"#708090", "snow":"#fffafa", "springgreen":"#00ff7f", "steelblue":"#4682b4", "tan":"#d2b48c", "teal":"#008080", "thistle":"#d8bfd8", "tomato":"#ff6347", "turquoise":"#40e0d0", "violet":"#ee82ee", "wheat":"#f5deb3", "white":"#ffffff", "whitesmoke":"#f5f5f5", "yellow":"#ffff00", "yellowgreen":"#9acd32"
};
// if the value is in the list of colors convert it.
if (v.toLowerCase() in colours) {
v = colours[v.toLowerCase()];
}
}
// Create a new property row with a clear button
const newPropHTML = `
<div class="property-item" style="display:flex;align-items:center;">
<button style="color:darkred;margin-right:5px;" type="button" onclick="UI.indicators.remove_prop(this)">&#10008;</button>
<span>${n}: ${v}</span>
</div>`;
// Insert the new property into the property list
document.getElementById("new_prop_list").insertAdjacentHTML('beforeend', newPropHTML);
// Update the hidden property object with the new property
const propObj = JSON.parse(document.getElementById("new_prop_obj").value || '{}');
propObj[n] = v;
document.getElementById("new_prop_obj").value = JSON.stringify(propObj);
// Clear the input fields
document.getElementById("new_prop_name").value = '';
document.getElementById("new_prop_value").value = '';
}
// Function to remove a property from the list
remove_prop(buttonElement) {
const propertyDiv = buttonElement.parentElement;
const propertyText = propertyDiv.querySelector('span').textContent;
const [propName] = propertyText.split(':');
// Remove the property div from the DOM
propertyDiv.remove();
// Remove the property from the hidden input object
const propObj = JSON.parse(document.getElementById("new_prop_obj").value || '{}');
delete propObj[propName.trim()];
document.getElementById("new_prop_obj").value = JSON.stringify(propObj);
}
// Call to display Create new signal dialog.
open_form() {
// Show the form
document.getElementById("new_ind_form").style.display = "grid";
// Prefill the form fields with the current chart data
// Always use the current chart view values to ensure indicator is created
// for the currently viewed exchange/symbol/timeframe
const marketField = document.getElementById('ei_symbol');
const timeframeField = document.getElementById('ei_timeframe');
const exchangeField = document.getElementById('ei_exchange_name');
// Always set to current chart view values
if (window.UI.data.trading_pair) {
marketField.value = window.UI.data.trading_pair;
}
if (window.UI.data.interval) {
timeframeField.value = window.UI.data.interval;
}
if (window.UI.data.exchange) {
exchangeField.value = window.UI.data.exchange;
}
}
// Call to hide Create new signal dialog.
close_form() { document.getElementById("new_ind_form").style.display = "none"; }
submit_new_i() {
/* Populates a hidden <input> with a value from another element then submits the form
Used in the create indicator panel.*/
// Perform validation
const name = document.getElementById('newi_name').value;
const type = document.getElementById('newi_type').value;
let market = document.getElementById('ei_symbol').value;
const timeframe = document.getElementById('ei_timeframe').value;
const exchange = document.getElementById('ei_exchange_name').value;
let errorMsg = '';
if (!name) {
errorMsg += 'Indicator name is required.\n';
}
if (!type) {
errorMsg += 'Indicator type is required.\n';
}
if (!market) {
errorMsg += 'Indicator market is required.\n';
}
if (!timeframe) {
errorMsg += 'Timeframe is required.\n';
}
if (!exchange) {
errorMsg += 'Exchange name is required.\n';
}
if (errorMsg) {
alert(errorMsg); // Display the error messages
return; // Stop form submission if there are errors
}
// Collect and update properties
const propObj = {}
// Optionally add name, type, market, timeframe, and exchange to the properties if needed
propObj["name"] = name;
propObj["type"] = type;
propObj["source"] = {'market':market,'timeframe': timeframe,'exchange':exchange};
propObj["properties"] = document.getElementById("new_prop_obj").value;
// Call comms to send data to the server
this.comms.submitIndicator(propObj).then(response => {
if (response.success) {
window.location.reload(); // This triggers a full page refresh
} else {
alert(response.message || 'Failed to create a new Indicator.');
}
});
}
// Method to handle form submission
handleFormSubmit(event) {
event.preventDefault(); // Prevent default form submission behavior
// Get the form element
const form = event.target;
// Get all the checked checkboxes (indicators)
const checkboxes = form.querySelectorAll('input[name="indicator"]:checked');
let selectedIndicators = [];
checkboxes.forEach(function (checkbox) {
selectedIndicators.push(checkbox.value);
});
// Prepare the form data
const formData = new FormData(form);
formData.delete('indicator'); // Remove the single value (from original HTML behavior)
// Append all selected indicators as a single array-like structure
formData.append('indicator', JSON.stringify(selectedIndicators));
// Hide the popup immediately
const popup = document.getElementById('indicators');
if (popup) {
popup.style.display = 'none';
}
// Show a loading overlay on the page
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;justify-content:center;align-items:center;z-index:9999;';
overlay.innerHTML = '<div style="background:white;padding:20px 40px;border-radius:10px;font-size:18px;font-weight:bold;">Updating indicators...</div>';
document.body.appendChild(overlay);
// Send form data via AJAX (fetch)
fetch(form.action, {
method: form.method,
body: formData
}).then(response => {
if (response.ok) {
// Handle success (you can reload the page or update the UI)
window.location.reload();
} else {
// Remove overlay on error
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.remove();
alert('Failed to update indicators.');
}
}).catch(error => {
console.error('Error:', error);
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.remove();
alert('An error occurred while updating the indicators.');
});
}
// ==================== Card Display Methods ====================
/**
* Renders all indicators as icon cards in the indicators_list container
*/
renderIndicators() {
const container = document.getElementById('indicators_list');
if (!container) return;
container.innerHTML = '';
for (const [name, indicator] of Object.entries(this.indicators)) {
const item = document.createElement('div');
const isVisible = indicator.visible !== false;
item.className = `indicator-item ${isVisible ? 'visible' : 'hidden'}`;
item.setAttribute('data-indicator-name', name);
// Get indicator type symbol
const symbol = this.getIndicatorSymbol(indicator.type);
// Get current value from indicator_data
let displayValue = '--';
const data = this.indicator_data[name];
if (data && Array.isArray(data) && data.length > 0) {
const lastPoint = data[data.length - 1];
// Handle different indicator data formats
if (lastPoint.value !== undefined && lastPoint.value !== null) {
// Simple indicators (RSI, SMA, EMA, etc.)
displayValue = this._formatValue(lastPoint.value);
} else if (lastPoint.macd !== undefined) {
// MACD - show the MACD value
displayValue = this._formatValue(lastPoint.macd);
} else if (lastPoint.middle !== undefined) {
// Bollinger Bands - show middle value
displayValue = this._formatValue(lastPoint.middle);
} else if (lastPoint.upper !== undefined) {
// Other band indicators
displayValue = this._formatValue(lastPoint.upper);
}
}
const indicatorType = indicator.type || 'Unknown';
item.innerHTML = `
<button class="visibility-btn" onclick="UI.indicators.toggleVisibility('${name}'); event.stopPropagation();" title="${isVisible ? 'Hide' : 'Show'}">
${isVisible ? '👁' : '👁‍🗨'}
</button>
<button class="delete-btn" onclick="UI.indicators.deleteIndicatorCard('${name}'); event.stopPropagation();" title="Delete">×</button>
<div class="indicator-icon" title="${indicatorType}">
<span class="indicator-symbol">${symbol}</span>
</div>
<div class="indicator-name" title="${name}">${name}</div>
<div class="indicator-type">${indicatorType}</div>
<div class="indicator-value" id="indicator_card_value_${name}">${displayValue}</div>
`;
// Click to edit
item.addEventListener('click', () => this.openEditDialog(name));
container.appendChild(item);
}
}
/**
* Formats a numeric value for display
*/
_formatValue(value) {
if (value === null || value === undefined) return '--';
if (typeof value === 'number') {
// For large numbers, show fewer decimals
if (Math.abs(value) >= 1000) {
return value.toFixed(0);
} else if (Math.abs(value) >= 100) {
return value.toFixed(1);
} else {
return value.toFixed(2);
}
}
return String(value);
}
/**
* Returns an emoji/symbol for the given indicator type
*/
getIndicatorSymbol(type) {
// All keys are uppercase for consistent matching
const symbols = {
'RSI': '📊',
'MACD': '📈',
'SMA': '〰️',
'EMA': '〰️',
'LREG': '➡️',
'BOLBANDS': '📉',
'BOL%B': '📉',
'ATR': '📏',
'VOLUME': '📊',
};
if (!type) return '📈';
const upperType = type.toUpperCase();
// Check for candlestick patterns (they start with CDL_)
if (upperType.startsWith('CDL_')) {
return '🕯️';
}
return symbols[upperType] || '📈'; // default chart emoji
}
/**
* Opens the edit dialog for an indicator
*/
openEditDialog(indicatorName) {
const indicator = this.indicators[indicatorName];
if (!indicator) return;
const form = document.getElementById('edit_indicator_form');
if (!form) {
console.error('Edit indicator form not found');
return;
}
// Populate form fields
document.getElementById('edit_indicator_name').value = indicatorName;
document.getElementById('edit_ind_display_name').value = indicatorName;
document.getElementById('edit_ind_type').value = indicator.type || 'Unknown';
document.getElementById('edit_ind_visible').checked = indicator.visible !== false;
// Source fields
const source = indicator.source || {};
document.getElementById('edit_ind_market').value = source.market || '';
document.getElementById('edit_ind_timeframe').value = source.timeframe || '';
document.getElementById('edit_ind_exchange').value = source.exchange || '';
// Color
document.getElementById('edit_ind_color').value = indicator.color || '#667eea';
// Render dynamic properties
const propsContainer = document.getElementById('edit_ind_properties');
propsContainer.innerHTML = '';
// Properties to exclude from dynamic rendering
const excludeProps = ['type', 'value', 'color', 'visible', 'source', 'name',
'color_1', 'color_2', 'color_3', 'bullish_color', 'bearish_color'];
for (const [key, value] of Object.entries(indicator)) {
if (!excludeProps.includes(key)) {
const div = document.createElement('div');
div.innerHTML = `
<label style="font-size: 12px;">${key}</label>
<input type="text" name="${key}" value="${value}" style="width: 100%; margin-top: 3px;">
`;
propsContainer.appendChild(div);
}
}
// Show and position the form
form.style.display = 'block';
form.style.left = '50%';
form.style.top = '50%';
form.style.transform = 'translate(-50%, -50%)';
form.style.position = 'fixed';
form.style.zIndex = '1000';
}
/**
* Closes the edit dialog
*/
closeEditDialog() {
const form = document.getElementById('edit_indicator_form');
if (form) {
form.style.display = 'none';
}
}
/**
* Saves changes from the edit dialog
*/
saveEditDialog() {
const indicatorName = document.getElementById('edit_indicator_name').value;
const formObj = {
name: indicatorName,
visible: document.getElementById('edit_ind_visible').checked,
color: document.getElementById('edit_ind_color').value,
source: {
market: document.getElementById('edit_ind_market').value,
timeframe: document.getElementById('edit_ind_timeframe').value,
exchange: document.getElementById('edit_ind_exchange').value
},
properties: {}
};
// Gather dynamic properties
const propsContainer = document.getElementById('edit_ind_properties');
propsContainer.querySelectorAll('input').forEach(input => {
const value = parseFloat(input.value);
formObj.properties[input.name] = isNaN(value) ? input.value : value;
});
this.comms.updateIndicator(formObj).then(response => {
if (response.success) {
this.closeEditDialog();
// Refresh page to reload indicators with new settings
window.location.reload();
} else {
alert('Failed to update indicator: ' + (response.message || 'Unknown error'));
}
}).catch(error => {
console.error('Error updating indicator:', error);
alert('An error occurred while updating the indicator.');
});
}
/**
* Deletes an indicator card with confirmation
*/
deleteIndicatorCard(indicatorName) {
if (!confirm(`Delete indicator "${indicatorName}"?`)) return;
this.comms.deleteIndicator(indicatorName).then(response => {
if (response.success) {
// Remove from chart
if (this.i_objs[indicatorName]) {
// Determine which chart the indicator is on
let chart;
const indicator = this.i_objs[indicatorName];
const indicatorType = indicator.constructor.name;
if (indicatorName.includes('RSI') || indicatorType === 'RSI') {
chart = window.UI.charts.chart2;
} else if (indicatorName.includes('MACD') || indicatorType === 'MACD') {
chart = window.UI.charts.chart3;
} else if (indicatorName.includes('%B') || indicatorType === 'BollingerPercentB') {
chart = window.UI.charts.chart4;
} else if (indicatorType === 'CandlestickPattern') {
chart = window.UI.charts.chart5;
} else {
chart = window.UI.charts.chart_1;
}
indicator.removeFromChart(chart);
delete this.i_objs[indicatorName];
iOutput.clear_legend(indicatorName);
}
// Remove from indicators object
delete this.indicators[indicatorName];
// Re-render the cards
this.renderIndicators();
} else {
alert('Failed to delete indicator: ' + (response.message || 'Unknown error'));
}
}).catch(error => {
console.error('Error deleting indicator:', error);
alert('An error occurred while deleting the indicator.');
});
}
/**
* Toggles visibility of an indicator on the chart
*/
toggleVisibility(indicatorName) {
const indicator = this.indicators[indicatorName];
if (!indicator) return;
// Toggle the visibility
indicator.visible = !indicator.visible;
// Save to server and reload to apply chart changes
this.comms.updateIndicator({
name: indicatorName,
visible: indicator.visible
}).then(response => {
if (response.success) {
// Refresh page to properly update chart display
window.location.reload();
} else {
// Revert the toggle if save failed
indicator.visible = !indicator.visible;
alert('Failed to update visibility');
}
}).catch(error => {
console.error('Error updating visibility:', error);
indicator.visible = !indicator.visible;
});
}
/**
* Makes a dialog draggable by its header
*/
_makeDialogDraggable(dialog, handle) {
if (!dialog || !handle) return;
let offsetX = 0, offsetY = 0, isDragging = false;
handle.style.cursor = 'move';
handle.onmousedown = (e) => {
if (e.target.tagName === 'BUTTON') return; // Don't drag when clicking buttons
isDragging = true;
offsetX = e.clientX - dialog.offsetLeft;
offsetY = e.clientY - dialog.offsetTop;
document.onmousemove = onMouseMove;
document.onmouseup = onMouseUp;
};
function onMouseMove(e) {
if (!isDragging) return;
dialog.style.left = (e.clientX - offsetX) + 'px';
dialog.style.top = (e.clientY - offsetY) + 'px';
dialog.style.transform = 'none';
}
function onMouseUp() {
isDragging = false;
document.onmousemove = null;
document.onmouseup = null;
}
}
}