540 lines
16 KiB
JavaScript
540 lines
16 KiB
JavaScript
/**
|
|
* FormationsUIManager - Handles DOM updates and formation card rendering
|
|
*/
|
|
class FormationsUIManager {
|
|
constructor() {
|
|
this.targetEl = null;
|
|
this.drawingControlsEl = null;
|
|
this.nameInputEl = null;
|
|
this.onDeleteFormation = null;
|
|
this.onEditFormation = null;
|
|
}
|
|
|
|
/**
|
|
* Initializes the UI elements.
|
|
* @param {string} targetId - ID of the formations list container
|
|
*/
|
|
initUI(targetId) {
|
|
this.targetEl = document.getElementById(targetId);
|
|
if (!this.targetEl) {
|
|
console.warn(`Formations container "${targetId}" not found.`);
|
|
}
|
|
|
|
this.drawingControlsEl = document.getElementById('formation_drawing_controls');
|
|
this.nameInputEl = document.getElementById('formation_name_input');
|
|
}
|
|
|
|
/**
|
|
* Register callback for delete formation.
|
|
* @param {Function} callback - Function to call when delete is clicked
|
|
*/
|
|
registerDeleteCallback(callback) {
|
|
this.onDeleteFormation = callback;
|
|
}
|
|
|
|
/**
|
|
* Register callback for edit formation.
|
|
* @param {Function} callback - Function to call when edit is clicked
|
|
*/
|
|
registerEditCallback(callback) {
|
|
this.onEditFormation = callback;
|
|
}
|
|
|
|
/**
|
|
* Show drawing controls.
|
|
*/
|
|
showDrawingControls() {
|
|
if (this.drawingControlsEl) {
|
|
this.drawingControlsEl.style.display = 'block';
|
|
}
|
|
if (this.nameInputEl) {
|
|
this.nameInputEl.value = '';
|
|
this.nameInputEl.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide drawing controls.
|
|
*/
|
|
hideDrawingControls() {
|
|
if (this.drawingControlsEl) {
|
|
this.drawingControlsEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the formation name from input.
|
|
* @returns {string} Formation name
|
|
*/
|
|
getFormationName() {
|
|
return this.nameInputEl ? this.nameInputEl.value.trim() : '';
|
|
}
|
|
|
|
/**
|
|
* Render all formations as cards.
|
|
* @param {Array} formations - List of formation objects
|
|
*/
|
|
renderFormations(formations) {
|
|
if (!this.targetEl) {
|
|
console.warn("Formations container not initialized");
|
|
return;
|
|
}
|
|
|
|
this.targetEl.innerHTML = '';
|
|
|
|
if (!formations || formations.length === 0) {
|
|
this.targetEl.innerHTML = '<p style="color: #888; font-size: 12px; padding: 10px;">No formations yet. Click a button above to draw one.</p>';
|
|
return;
|
|
}
|
|
|
|
formations.forEach(formation => {
|
|
const card = this._createFormationCard(formation);
|
|
this.targetEl.appendChild(card);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a formation card element.
|
|
* @param {Object} formation - Formation data
|
|
* @returns {HTMLElement} Card element
|
|
*/
|
|
_createFormationCard(formation) {
|
|
const card = document.createElement('div');
|
|
card.className = 'formation-item';
|
|
card.dataset.tblKey = formation.tbl_key;
|
|
card.dataset.type = formation.formation_type;
|
|
|
|
// Format type for display
|
|
const typeDisplay = this._formatType(formation.formation_type);
|
|
|
|
card.innerHTML = `
|
|
<div class="formation-icon">
|
|
<span class="formation-name">${this._escapeHtml(formation.name)}</span>
|
|
<span class="formation-type">${typeDisplay}</span>
|
|
</div>
|
|
<button class="edit-button" title="Edit">✎</button>
|
|
<button class="delete-button" title="Delete">×</button>
|
|
<div class="formation-hover">
|
|
<strong>${this._escapeHtml(formation.name)}</strong>
|
|
<div class="formation-details">
|
|
<span>Type: ${typeDisplay}</span>
|
|
<span><span class="formation-color-dot" style="background: ${formation.color}"></span>Color</span>
|
|
<span>Scope: ${formation.exchange}/${formation.market}/${formation.timeframe}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add click handlers
|
|
const deleteBtn = card.querySelector('.delete-button');
|
|
deleteBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
if (this.onDeleteFormation) {
|
|
this.onDeleteFormation(formation.tbl_key, formation.name);
|
|
}
|
|
});
|
|
|
|
const editBtn = card.querySelector('.edit-button');
|
|
editBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
if (this.onEditFormation) {
|
|
this.onEditFormation(formation.tbl_key);
|
|
}
|
|
});
|
|
|
|
// Card click to select
|
|
card.addEventListener('click', () => {
|
|
this._selectCard(card);
|
|
if (this.onEditFormation) {
|
|
this.onEditFormation(formation.tbl_key);
|
|
}
|
|
});
|
|
|
|
return card;
|
|
}
|
|
|
|
/**
|
|
* Select a card visually.
|
|
* @param {HTMLElement} card - Card to select
|
|
*/
|
|
_selectCard(card) {
|
|
// Deselect all
|
|
this.targetEl.querySelectorAll('.formation-item').forEach(c => {
|
|
c.classList.remove('selected');
|
|
});
|
|
// Select this one
|
|
card.classList.add('selected');
|
|
}
|
|
|
|
/**
|
|
* Format formation type for display.
|
|
* @param {string} type - Formation type
|
|
* @returns {string} Formatted type
|
|
*/
|
|
_formatType(type) {
|
|
const typeMap = {
|
|
'support_resistance': 'Line',
|
|
'channel': 'Channel',
|
|
'triangle': 'Triangle',
|
|
'head_shoulders': 'H&S',
|
|
'double_bottom': 'Double Bottom',
|
|
'double_top': 'Double Top'
|
|
};
|
|
return typeMap[type] || type;
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS.
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string
|
|
*/
|
|
_escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* FormationsDataManager - Manages in-memory formations data
|
|
*/
|
|
class FormationsDataManager {
|
|
constructor() {
|
|
this.formations = [];
|
|
}
|
|
|
|
/**
|
|
* Set all formations.
|
|
* @param {Array} formations - List of formations
|
|
*/
|
|
setFormations(formations) {
|
|
this.formations = Array.isArray(formations) ? formations : [];
|
|
}
|
|
|
|
/**
|
|
* Get all formations.
|
|
* @returns {Array} List of formations
|
|
*/
|
|
getAllFormations() {
|
|
return this.formations;
|
|
}
|
|
|
|
/**
|
|
* Add a new formation.
|
|
* @param {Object} formation - Formation to add
|
|
*/
|
|
addFormation(formation) {
|
|
if (formation) {
|
|
this.formations.push(formation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing formation.
|
|
* @param {Object} updatedFormation - Updated formation data
|
|
*/
|
|
updateFormation(updatedFormation) {
|
|
const index = this.formations.findIndex(f => f.tbl_key === updatedFormation.tbl_key);
|
|
if (index !== -1) {
|
|
this.formations[index] = { ...this.formations[index], ...updatedFormation };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a formation by tbl_key.
|
|
* @param {string} tblKey - Formation tbl_key to remove
|
|
*/
|
|
removeFormation(tblKey) {
|
|
this.formations = this.formations.filter(f => f.tbl_key !== tblKey);
|
|
}
|
|
|
|
/**
|
|
* Get a formation by tbl_key.
|
|
* @param {string} tblKey - Formation tbl_key
|
|
* @returns {Object|null} Formation or null
|
|
*/
|
|
getFormation(tblKey) {
|
|
return this.formations.find(f => f.tbl_key === tblKey) || null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Formations - Main coordinator class for formations feature
|
|
*/
|
|
class Formations {
|
|
constructor(ui) {
|
|
this.ui = ui;
|
|
this.comms = ui?.data?.comms;
|
|
this.data = ui?.data;
|
|
|
|
this.dataManager = new FormationsDataManager();
|
|
this.uiManager = new FormationsUIManager();
|
|
this.overlay = null;
|
|
|
|
// Current drawing state
|
|
this.drawingMode = null;
|
|
this.currentScope = null;
|
|
|
|
// Set up callbacks
|
|
this.uiManager.registerDeleteCallback(this.deleteFormation.bind(this));
|
|
this.uiManager.registerEditCallback(this.selectFormation.bind(this));
|
|
|
|
this._initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Initialize the formations system.
|
|
* @param {string} targetId - ID of formations list container
|
|
*/
|
|
initialize(targetId) {
|
|
try {
|
|
this.uiManager.initUI(targetId);
|
|
|
|
if (!this.comms) {
|
|
console.error("Communications instance not available for Formations");
|
|
return;
|
|
}
|
|
|
|
// Register socket handlers
|
|
this.registerSocketHandlers();
|
|
|
|
// Get current scope from chart data
|
|
this.currentScope = {
|
|
exchange: this.data?.exchange || window.bt_data?.exchange || 'kucoin',
|
|
market: this.data?.trading_pair || window.bt_data?.trading_pair || 'BTC/USDT',
|
|
timeframe: this.data?.timeframe || window.bt_data?.timeframe || '1h'
|
|
};
|
|
|
|
// Fetch formations for current scope
|
|
this.fetchFormations();
|
|
|
|
this._initialized = true;
|
|
console.log("Formations initialized for scope:", this.currentScope);
|
|
|
|
} catch (error) {
|
|
console.error("Error initializing Formations:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the SVG overlay for drawing.
|
|
* @param {string} chartContainerId - ID of chart container
|
|
* @param {Object} chart - Lightweight Charts instance
|
|
* @param {Object} candleSeries - Candlestick series for coordinate conversion
|
|
*/
|
|
initOverlay(chartContainerId, chart, candleSeries) {
|
|
if (typeof FormationOverlay !== 'undefined') {
|
|
this.overlay = new FormationOverlay(chartContainerId, chart, candleSeries);
|
|
this.overlay.setOnSaveCallback(this.saveFormation.bind(this));
|
|
console.log("Formation overlay initialized");
|
|
} else {
|
|
console.warn("FormationOverlay class not loaded");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register socket handlers for formations.
|
|
*/
|
|
registerSocketHandlers() {
|
|
this.comms.on('formations', this.handleFormationsResponse.bind(this));
|
|
this.comms.on('formation_created', this.handleFormationCreated.bind(this));
|
|
this.comms.on('formation_updated', this.handleFormationUpdated.bind(this));
|
|
this.comms.on('formation_deleted', this.handleFormationDeleted.bind(this));
|
|
this.comms.on('formation_error', this.handleFormationError.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Fetch formations for current scope.
|
|
*/
|
|
fetchFormations() {
|
|
if (!this.comms || !this.currentScope) return;
|
|
|
|
this.comms.sendToApp('request_formations', this.currentScope);
|
|
}
|
|
|
|
// ================ Socket Handlers ================
|
|
|
|
/**
|
|
* Handle formations list response.
|
|
* @param {Object} data - Response with formations array
|
|
*/
|
|
handleFormationsResponse(data) {
|
|
console.log("Received formations:", data);
|
|
const formations = data.formations || [];
|
|
this.dataManager.setFormations(formations);
|
|
this.uiManager.renderFormations(formations);
|
|
|
|
// Render on overlay if available
|
|
if (this.overlay) {
|
|
this.overlay.clearAllFormations();
|
|
formations.forEach(f => this.overlay.renderFormation(f));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle formation created event.
|
|
* @param {Object} data - Response with formation data
|
|
*/
|
|
handleFormationCreated(data) {
|
|
console.log("Formation created:", data);
|
|
if (data.success && data.formation) {
|
|
this.dataManager.addFormation(data.formation);
|
|
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
|
|
|
if (this.overlay) {
|
|
this.overlay.renderFormation(data.formation);
|
|
}
|
|
|
|
this.uiManager.hideDrawingControls();
|
|
this.drawingMode = null;
|
|
} else {
|
|
alert(`Failed to create formation: ${data.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle formation updated event.
|
|
* @param {Object} data - Response with updated formation data
|
|
*/
|
|
handleFormationUpdated(data) {
|
|
console.log("Formation updated:", data);
|
|
if (data.success && data.formation) {
|
|
this.dataManager.updateFormation(data.formation);
|
|
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
|
|
|
if (this.overlay) {
|
|
this.overlay.updateFormation(data.formation);
|
|
}
|
|
} else {
|
|
alert(`Failed to update formation: ${data.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle formation deleted event.
|
|
* @param {Object} data - Response with tbl_key of deleted formation
|
|
*/
|
|
handleFormationDeleted(data) {
|
|
console.log("Formation deleted:", data);
|
|
if (data.success && data.tbl_key) {
|
|
this.dataManager.removeFormation(data.tbl_key);
|
|
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
|
|
|
if (this.overlay) {
|
|
this.overlay.removeFormation(data.tbl_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle formation error.
|
|
* @param {Object} data - Error data
|
|
*/
|
|
handleFormationError(data) {
|
|
console.error("Formation error:", data.message);
|
|
alert(`Formation error: ${data.message}`);
|
|
}
|
|
|
|
// ================ Drawing Methods ================
|
|
|
|
/**
|
|
* Start drawing a new formation.
|
|
* @param {string} type - Formation type ('support_resistance', 'channel')
|
|
*/
|
|
startDrawing(type) {
|
|
console.log("Starting drawing mode:", type);
|
|
this.drawingMode = type;
|
|
|
|
// Show drawing controls
|
|
this.uiManager.showDrawingControls();
|
|
|
|
// Tell overlay to start drawing
|
|
if (this.overlay) {
|
|
this.overlay.startDrawing(type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Complete the current drawing.
|
|
*/
|
|
completeDrawing() {
|
|
const name = this.uiManager.getFormationName();
|
|
if (!name) {
|
|
alert("Please enter a formation name");
|
|
return;
|
|
}
|
|
|
|
if (this.overlay) {
|
|
this.overlay.completeDrawing(name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel the current drawing.
|
|
*/
|
|
cancelDrawing() {
|
|
this.drawingMode = null;
|
|
this.uiManager.hideDrawingControls();
|
|
|
|
if (this.overlay) {
|
|
this.overlay.cancelDrawing();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save a formation (callback from overlay).
|
|
* @param {Object} formationData - Formation data to save
|
|
*/
|
|
saveFormation(formationData) {
|
|
if (!this.comms) return;
|
|
|
|
const payload = {
|
|
...formationData,
|
|
...this.currentScope
|
|
};
|
|
|
|
this.comms.sendToApp('new_formation', payload);
|
|
}
|
|
|
|
/**
|
|
* Select a formation for editing.
|
|
* @param {string} tblKey - Formation tbl_key
|
|
*/
|
|
selectFormation(tblKey) {
|
|
console.log("Selecting formation:", tblKey);
|
|
if (this.overlay) {
|
|
this.overlay.selectFormation(tblKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a formation.
|
|
* @param {string} tblKey - Formation tbl_key
|
|
* @param {string} name - Formation name (for confirmation)
|
|
*/
|
|
deleteFormation(tblKey, name) {
|
|
if (!confirm(`Delete formation "${name}"?`)) {
|
|
return;
|
|
}
|
|
|
|
if (this.comms) {
|
|
this.comms.sendToApp('delete_formation', { tbl_key: tblKey });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a formation's lines (called from overlay after drag).
|
|
* @param {string} tblKey - Formation tbl_key
|
|
* @param {Object} linesData - Updated lines data
|
|
*/
|
|
updateFormationLines(tblKey, linesData) {
|
|
if (!this.comms) return;
|
|
|
|
this.comms.sendToApp('edit_formation', {
|
|
tbl_key: tblKey,
|
|
lines_json: JSON.stringify(linesData)
|
|
});
|
|
}
|
|
}
|