feat: Implement settings panel for vertical and horizontal lines

- Add default settings (color, width, style, opacity) for horizontal_line and vertical_line
- Enable settings panel to open on double-click for horizontal/vertical lines
- Update rendering to support full styling (color, width, style, opacity) with visual selection handles
- Show/hide Text tab based on drawing type (trend_line/ray/rectangle only)
- Add proper drag handling to move entire horizontal/vertical lines
This commit is contained in:
DiTus
2026-03-21 23:41:05 +01:00
parent 43fa8efda7
commit 30ac99479f
2 changed files with 130 additions and 27 deletions

View File

@ -151,7 +151,7 @@
<!-- Tabs --> <!-- Tabs -->
<div class="flex border-b border-[#2d3a4f] mb-4"> <div class="flex border-b border-[#2d3a4f] mb-4">
<button class="flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium" id="tlTabStyle" onclick="window.switchTLTab('style')">Style</button> <button class="flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium" id="tlTabStyle" onclick="window.switchTLTab('style')">Style</button>
<button class="flex-1 py-2 text-center text-gray-400 hover:text-white" id="tlTabText" onclick="window.switchTLTab('text')">Text</button> <button class="flex-1 py-2 text-center text-gray-400 hover:text-white" id="tlTabText" onclick="window.switchTLTab('text')" style="display: none;">Text</button>
</div> </div>
<!-- Style Tab Content --> <!-- Style Tab Content -->

View File

@ -30,6 +30,18 @@ export class DrawingManager {
italic: false, italic: false,
alignVert: 'top', alignVert: 'top',
alignHorz: 'left' alignHorz: 'left'
},
horizontal_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100
},
vertical_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100
} }
}; };
@ -55,7 +67,7 @@ export class DrawingManager {
if (hit) { if (hit) {
this.selectedDrawing = hit.drawing; this.selectedDrawing = hit.drawing;
// Only open panel for supported types // Only open panel for supported types
if (['trend_line', 'ray', 'rectangle'].includes(hit.drawing.type)) { if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) {
this.toggleSettingsPanel(true); this.toggleSettingsPanel(true);
} }
this.update(); this.update();
@ -337,11 +349,11 @@ export class DrawingManager {
return; return;
} }
const defs = this.defaults.trend_line; const color = this.defaults.trend_line.color;
const color = defs.color;
const p1 = { time: pos.time, price: pos.price }; const p1 = { time: pos.time, price: pos.price };
const p2 = { time: pos.time, price: pos.price }; const p2 = { time: pos.time, price: pos.price };
const defs = this.defaults.trend_line;
if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') { if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
this.currentDrawing = { this.currentDrawing = {
type: this.activeTool, p1, p2, type: this.activeTool, p1, p2,
@ -358,10 +370,26 @@ export class DrawingManager {
alignHorz: defs.alignHorz alignHorz: defs.alignHorz
}; };
} else if (this.activeTool === 'horizontal_line') { } else if (this.activeTool === 'horizontal_line') {
this.drawings.push({ type: 'horizontal_line', price: pos.price, color }); const defs = this.defaults.horizontal_line;
this.drawings.push({
type: 'horizontal_line',
price: pos.price,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'vertical_line') { } else if (this.activeTool === 'vertical_line') {
this.drawings.push({ type: 'vertical_line', time: pos.time, color }); const defs = this.defaults.vertical_line;
this.drawings.push({
type: 'vertical_line',
time: pos.time,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'fib_retracement') { } else if (this.activeTool === 'fib_retracement') {
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' }; this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
@ -526,10 +554,10 @@ export class DrawingManager {
} }
} else if (d.price !== undefined && d.type === 'horizontal_line') { } else if (d.price !== undefined && d.type === 'horizontal_line') {
const dy = this.series.priceToCoordinate(d.price); const dy = this.series.priceToCoordinate(d.price);
if (dy !== null && Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' }; if (dy !== null && Math.abs(y - dy) < threshold) return { drawing: d, part: 'all' };
} else if (d.time !== undefined && d.type === 'vertical_line') { } else if (d.time !== undefined && d.type === 'vertical_line') {
const dx = this.timeToX(d.time); const dx = this.timeToX(d.time);
if (dx !== null && Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' }; if (dx !== null && Math.abs(x - dx) < threshold) return { drawing: d, part: 'all' };
} else if (d.time !== undefined && d.price !== undefined) { } else if (d.time !== undefined && d.price !== undefined) {
const dx = this.timeToX(d.time); const dx = this.timeToX(d.time);
const dy = this.series.priceToCoordinate(d.price); const dy = this.series.priceToCoordinate(d.price);
@ -752,18 +780,44 @@ export class DrawingManager {
} else if (d.type === 'horizontal_line') { } else if (d.type === 'horizontal_line') {
const y = series.priceToCoordinate(d.price); const y = series.priceToCoordinate(d.price);
if (y !== null) { if (y !== null) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(-1000, y); ctx.moveTo(-1000, y);
ctx.lineTo(scope.mediaSize.width + 1000, y); ctx.lineTo(scope.mediaSize.width + 1000, y);
ctx.stroke(); ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
ctx.restore();
} }
} else if (d.type === 'vertical_line') { } else if (d.type === 'vertical_line') {
const x = this.timeToX(d.time); const x = this.timeToX(d.time);
if (x !== null) { if (x !== null) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, -1000); ctx.moveTo(x, -1000);
ctx.lineTo(x, scope.mediaSize.height + 1000); ctx.lineTo(x, scope.mediaSize.height + 1000);
ctx.stroke(); ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
ctx.restore();
} }
} else if (d.type === 'fib_retracement') { } else if (d.type === 'fib_retracement') {
const x1 = this.timeToX(d.p1.time); const x1 = this.timeToX(d.p1.time);
@ -1017,8 +1071,17 @@ export class DrawingManager {
window.switchTLTab = (tab) => { window.switchTLTab = (tab) => {
this.activeTLTab = tab; this.activeTLTab = tab;
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
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('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';
const textTab = document.getElementById('tlTabText');
if (isLineWithText) {
textTab.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';
textTab.style.display = 'block';
} else {
textTab.style.display = 'none';
}
document.getElementById('tlContentStyle').className = tab === 'style' ? 'block' : 'hidden'; document.getElementById('tlContentStyle').className = tab === 'style' ? 'block' : 'hidden';
document.getElementById('tlContentText').className = tab === 'text' ? 'block' : 'hidden'; document.getElementById('tlContentText').className = tab === 'text' ? 'block' : 'hidden';
@ -1032,11 +1095,11 @@ export class DrawingManager {
window.setTLThickness = (width) => this.applySettings('width', width); window.setTLThickness = (width) => this.applySettings('width', width);
window.setTLStyle = (style) => this.applySettings('style', style); window.setTLStyle = (style) => this.applySettings('style', style);
window.toggleTLBold = () => { window.toggleTLBold = () => {
const current = this.selectedDrawing ? this.selectedDrawing.bold : this.defaults.trend_line.bold; const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
this.applySettings('bold', !current); this.applySettings('bold', !current);
}; };
window.toggleTLItalic = () => { window.toggleTLItalic = () => {
const current = this.selectedDrawing ? this.selectedDrawing.italic : this.defaults.trend_line.italic; const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
this.applySettings('italic', !current); this.applySettings('italic', !current);
}; };
@ -1129,6 +1192,14 @@ export class DrawingManager {
if (show) { if (show) {
panel.classList.remove('hidden'); panel.classList.remove('hidden');
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
if (!isLineWithText) {
this.activeTLTab = 'style';
document.getElementById('tlTabStyle').className = 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium';
document.getElementById('tlTabText').className = 'flex-1 py-2 text-center text-gray-400 hover:text-white';
document.getElementById('tlContentStyle').className = 'block';
document.getElementById('tlContentText').className = 'hidden';
}
this.updateSettingsPanelUI(); this.updateSettingsPanelUI();
} else { } else {
panel.classList.add('hidden'); panel.classList.add('hidden');
@ -1136,7 +1207,7 @@ export class DrawingManager {
} }
updateSettingsPanelUI() { updateSettingsPanelUI() {
const settings = this.selectedDrawing || this.defaults.trend_line; const settings = this.selectedDrawing || this.defaults[this.selectedDrawing ? this.selectedDrawing.type : 'trend_line'];
document.querySelectorAll('#tlColorGrid .color-swatch').forEach(el => { document.querySelectorAll('#tlColorGrid .color-swatch').forEach(el => {
if (el.style.backgroundColor === settings.color) el.classList.add('active'); if (el.style.backgroundColor === settings.color) el.classList.add('active');
@ -1154,28 +1225,60 @@ export class DrawingManager {
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style); btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
}); });
document.querySelectorAll('#tlTextColorGrid .color-swatch').forEach(el => { const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
if (el.style.backgroundColor === settings.textColor) el.classList.add('active');
else el.classList.remove('active');
});
if (document.getElementById('tlTextColorBtn')) { const textColorBtn = document.getElementById('tlTextColorBtn');
document.getElementById('tlTextColorBtn').style.backgroundColor = settings.textColor || settings.color; const textPicker = document.getElementById('tlTextColorPicker');
const fontInput = document.getElementById('tlFontSize');
const textInput = document.getElementById('tlTextInput');
const alignVert = document.getElementById('tlAlignVert');
const alignHorz = document.getElementById('tlAlignHorz');
const boldBtn = document.getElementById('tlBoldBtn');
const italicBtn = document.getElementById('tlItalicBtn');
if (isLineWithText) {
if (textColorBtn && textPicker) {
textColorBtn.style.backgroundColor = settings.textColor || settings.color;
textPicker.classList.remove('hidden');
}
fontInput.value = settings.fontSize || 14;
textInput.value = settings.text || '';
boldBtn.setAttribute('data-active', !!settings.bold);
italicBtn.setAttribute('data-active', !!settings.italic);
alignVert.value = settings.alignVert || 'top';
alignHorz.value = settings.alignHorz || 'left';
} else {
if (textPicker) textPicker.classList.add('hidden');
} }
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) { applySettings(key, value) {
if (this.selectedDrawing) { if (this.selectedDrawing) {
this.selectedDrawing[key] = value; if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
const isLineWithText = ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
if (!isLineWithText) return;
this.selectedDrawing[key] = value;
} else {
this.selectedDrawing[key] = value;
}
this.update(); this.update();
} else { } else {
this.defaults.trend_line[key] = value; const drawingType = this.selectedDrawing ? this.selectedDrawing.type : 'trend_line';
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
}
} else {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
} else if (drawingType === 'horizontal_line') {
this.defaults.horizontal_line[key] = value;
} else if (drawingType === 'vertical_line') {
this.defaults.vertical_line[key] = value;
} else {
this.defaults.trend_line[key] = value;
}
}
} }
this.updateSettingsPanelUI(); this.updateSettingsPanelUI();
} }