feat: Implement draggable Trendline Settings Panel with Shift+Click and TF layout fixes

This commit is contained in:
DiTus
2026-03-21 23:28:23 +01:00
parent fd816edc54
commit 43fa8efda7
6 changed files with 485 additions and 32 deletions

View File

@ -458,7 +458,10 @@ export class TradingDashboard {
// Initialize Drawing Manager
this.drawingManager = new DrawingManager(this, chartContainer);
window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool);
window.activateDrawingTool = (tool, event) => {
const e = event || window.event;
this.drawingManager.setTool(tool, e);
};
// Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => {

View File

@ -16,6 +16,23 @@ export class DrawingManager {
this.dragMode = null;
this.isMouseDown = false;
// Default Settings
this.defaults = {
trend_line: {
color: '#2962ff',
width: 2,
style: 0, // 0: Solid, 1: Dashed, 2: Dotted
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
}
};
// Dragging offsets
this.dragStartPos = null;
this.startP1 = null;
@ -24,6 +41,7 @@ export class DrawingManager {
this.startTime = null;
this.init();
this.initSettingsPanel();
}
init() {
@ -31,6 +49,18 @@ export class DrawingManager {
// Mouse Events
container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
container.addEventListener('dblclick', (e) => {
const pos = this.getMousePos(e);
const hit = this.findHit(pos.x, pos.y);
if (hit) {
this.selectedDrawing = hit.drawing;
// Only open panel for supported types
if (['trend_line', 'ray', 'rectangle'].includes(hit.drawing.type)) {
this.toggleSettingsPanel(true);
}
this.update();
}
});
window.addEventListener('mousemove', (e) => this.handleMouseMove(e));
window.addEventListener('mouseup', (e) => this.handleMouseUp(e));
@ -164,7 +194,13 @@ export class DrawingManager {
});
}
setTool(tool) {
setTool(tool, event) {
// Shift + Click on trend_line button opens settings
if (tool === 'trend_line' && event && event.shiftKey) {
this.toggleSettingsPanel(true);
return;
}
this.activeTool = tool;
if (tool === 'cursor') {
@ -301,12 +337,26 @@ export class DrawingManager {
return;
}
const color = '#2962ff';
const defs = this.defaults.trend_line;
const color = defs.color;
const p1 = { time: pos.time, price: pos.price };
const p2 = { time: pos.time, price: pos.price };
if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
this.currentDrawing = { type: this.activeTool, p1, p2, color };
this.currentDrawing = {
type: this.activeTool, p1, p2,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
};
} else if (this.activeTool === 'horizontal_line') {
this.drawings.push({ type: 'horizontal_line', price: pos.price, color });
this.setTool(null);
@ -604,6 +654,20 @@ export class DrawingManager {
return x1 + ratio * (x2 - x1);
}
hexToRgba(hex, opacity) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex.slice(1, 3), 16);
g = parseInt(hex.slice(3, 5), 16);
b = parseInt(hex.slice(5, 7), 16);
}
return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
}
render(target) {
target.useMediaCoordinateSpace((scope) => {
const ctx = scope.context;
@ -615,8 +679,16 @@ export class DrawingManager {
allDrawings.forEach(d => {
const isSelected = d === this.selectedDrawing;
ctx.save();
ctx.strokeStyle = d.color;
ctx.lineWidth = isSelected ? 3 : 2;
// Base Style
const color = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.strokeStyle = color;
ctx.lineWidth = d.width || (isSelected ? 3 : 2);
if (d.style === 1) ctx.setLineDash([10, 5]); // Dashed
else if (d.style === 2) ctx.setLineDash([2, 2]); // Dotted
else ctx.setLineDash([]); // Solid
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
if (d.type === 'trend_line' || d.type === 'ray') {
@ -634,7 +706,29 @@ export class DrawingManager {
ctx.lineTo(x2, y2);
}
ctx.stroke();
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]); // Text should not be dashed
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || color;
ctx.textAlign = d.alignHorz || 'left';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
const tx = (x1 + x2) / 2;
const ty = (y1 + y2) / 2;
const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
ctx.fillText(d.text, tx, finalTy);
ctx.restore();
}
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
@ -829,8 +923,6 @@ export class DrawingManager {
const labelWidth = Math.max(...labelLines.map(l => ctx.measureText(l).width)) + 24;
const labelHeight = 65;
// Natural position: Centered horizontally at midpoint,
// vertically offset from the top/bottom edge.
const midX = (x1 + x2) / 2;
const topY = Math.min(y1, y2);
const bottomY = Math.max(y1, y2);
@ -881,4 +973,210 @@ export class DrawingManager {
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
initSettingsPanel() {
const panel = document.getElementById('trendlineSettingsPanel');
const header = document.getElementById('tlPanelHeader');
this.activeTLTab = 'style';
// Dragging Logic
let isDragging = false;
let startX, startY, initialX, initialY;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
panel.classList.remove('left-1/2', '-translate-x-1/2', 'top-14');
panel.style.left = initialX + 'px';
panel.style.top = initialY + 'px';
panel.style.margin = '0';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = (initialX + dx) + 'px';
panel.style.top = (initialY + dy) + 'px';
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
// Global functions for HTML event handlers
window.toggleTLSettings = (show) => this.toggleSettingsPanel(show);
window.switchTLTab = (tab) => {
this.activeTLTab = tab;
document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
document.getElementById('tlTabText').className = tab === 'text' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
document.getElementById('tlContentStyle').className = tab === 'style' ? 'block' : 'hidden';
document.getElementById('tlContentText').className = tab === 'text' ? 'block' : 'hidden';
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker) textPicker.classList.add('hidden');
this.updateSettingsPanelUI();
};
window.setTLThickness = (width) => this.applySettings('width', width);
window.setTLStyle = (style) => this.applySettings('style', style);
window.toggleTLBold = () => {
const current = this.selectedDrawing ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
this.applySettings('bold', !current);
};
window.toggleTLItalic = () => {
const current = this.selectedDrawing ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
this.applySettings('italic', !current);
};
// Initialize Color Grids
const colors = [
'#ffffff', '#e0e0e0', '#bdbdbd', '#9e9e9e', '#757575', '#616161', '#424242', '#212121', '#000000',
'#ef5350', '#ff9800', '#ffeb3b', '#66bb6a', '#009688', '#00bcd4', '#2962ff', '#673ab7', '#9c27b0', '#e91e63',
'#ffcdd2', '#ffe0b2', '#fff9c4', '#c8e6c9', '#b2dfdb', '#b2ebf2', '#bbdefb', '#d1c4e9', '#e1bee7', '#f8bbd0',
'#ef9a9a', '#ffcc80', '#fff59d', '#a5d6a7', '#80cbc4', '#80deea', '#90caf9', '#b39ddb', '#ce93d8', '#f48fb1',
'#e57373', '#ffb74d', '#fff176', '#81c784', '#4db6ac', '#4dd0e1', '#64b5f6', '#9575cd', '#ba68c8', '#f06292',
'#f44336', '#fb8c00', '#fdd835', '#4caf50', '#00897b', '#00acc1', '#1e88e5', '#5e35b1', '#8e24aa', '#d81b60',
'#c62828', '#ef6c00', '#f9a825', '#2e7d32', '#00695c', '#00838f', '#1565c0', '#4527a0', '#6a1b9a', '#ad1457',
'#b71c1c', '#e65100', '#f57f17', '#1b5e20', '#004d40', '#006064', '#0d47a1', '#311b92', '#4a148c', '#880e4f'
];
const styleGrid = document.getElementById('tlColorGrid');
const textGrid = document.getElementById('tlTextColorGrid');
const populateGrid = (gridEl, key, closePopupEl = null) => {
if (!gridEl) return;
gridEl.innerHTML = '';
colors.forEach(color => {
const btn = document.createElement('div');
btn.className = 'color-swatch';
btn.style.backgroundColor = color;
btn.onclick = (e) => {
e.stopPropagation();
this.applySettings(key, color);
if (closePopupEl) closePopupEl.classList.add('hidden');
};
gridEl.appendChild(btn);
});
};
populateGrid(styleGrid, 'color');
populateGrid(textGrid, 'textColor', document.getElementById('tlTextColorPicker'));
const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker');
if (textColorBtn && textPicker) {
textColorBtn.onclick = (e) => {
e.stopPropagation();
textPicker.classList.toggle('hidden');
};
}
// Bind Inputs
document.getElementById('tlOpacityInput').addEventListener('input', (e) => {
document.getElementById('tlOpacityValue').textContent = e.target.value + '%';
this.applySettings('opacity', parseInt(e.target.value));
});
document.getElementById('tlFontSize').addEventListener('change', (e) => {
this.applySettings('fontSize', parseInt(e.target.value));
});
document.getElementById('tlTextInput').addEventListener('input', (e) => {
this.applySettings('text', e.target.value);
});
document.getElementById('tlAlignVert').addEventListener('change', (e) => {
this.applySettings('alignVert', e.target.value);
});
document.getElementById('tlAlignHorz').addEventListener('change', (e) => {
this.applySettings('alignHorz', e.target.value);
});
// Close on click outside
document.addEventListener('mousedown', (e) => {
const panel = document.getElementById('trendlineSettingsPanel');
const toolbar = document.getElementById('drawingToolbar');
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker && !textPicker.classList.contains('hidden') && !textPicker.contains(e.target) && e.target !== document.getElementById('tlTextColorBtn')) {
textPicker.classList.add('hidden');
}
if (panel && !panel.classList.contains('hidden')) {
if (!panel.contains(e.target) && !toolbar.contains(e.target)) {
panel.classList.add('hidden');
}
}
});
}
toggleSettingsPanel(show = true) {
const panel = document.getElementById('trendlineSettingsPanel');
if (!panel) return;
if (show) {
panel.classList.remove('hidden');
this.updateSettingsPanelUI();
} else {
panel.classList.add('hidden');
}
}
updateSettingsPanelUI() {
const settings = this.selectedDrawing || this.defaults.trend_line;
document.querySelectorAll('#tlColorGrid .color-swatch').forEach(el => {
if (el.style.backgroundColor === settings.color) el.classList.add('active');
else el.classList.remove('active');
});
document.getElementById('tlOpacityInput').value = settings.opacity !== undefined ? settings.opacity : 100;
document.getElementById('tlOpacityValue').textContent = (settings.opacity !== undefined ? settings.opacity : 100) + '%';
document.querySelectorAll('.tl-thickness-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.thickness) === settings.width);
});
document.querySelectorAll('.tl-style-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
});
document.querySelectorAll('#tlTextColorGrid .color-swatch').forEach(el => {
if (el.style.backgroundColor === settings.textColor) el.classList.add('active');
else el.classList.remove('active');
});
if (document.getElementById('tlTextColorBtn')) {
document.getElementById('tlTextColorBtn').style.backgroundColor = settings.textColor || settings.color;
}
document.getElementById('tlFontSize').value = settings.fontSize || 14;
document.getElementById('tlTextInput').value = settings.text || '';
document.getElementById('tlBoldBtn').setAttribute('data-active', !!settings.bold);
document.getElementById('tlItalicBtn').setAttribute('data-active', !!settings.italic);
document.getElementById('tlAlignVert').value = settings.alignVert || 'top';
document.getElementById('tlAlignHorz').value = settings.alignHorz || 'left';
}
applySettings(key, value) {
if (this.selectedDrawing) {
this.selectedDrawing[key] = value;
this.update();
} else {
this.defaults.trend_line[key] = value;
}
this.updateSettingsPanelUI();
}
}