added new preview feature
This commit is contained in:
@ -1,11 +1,11 @@
|
||||
{
|
||||
"open_air": false,
|
||||
"axis": "abs",
|
||||
"cut": 0.279,
|
||||
"max": 1.5,
|
||||
"gain": 0.7,
|
||||
"start_freq": 2130.0,
|
||||
"stop_freq": 8230.0,
|
||||
"cut": 0.0,
|
||||
"max": 5.0,
|
||||
"gain": 0.0,
|
||||
"start_freq": 100.0,
|
||||
"stop_freq": 8800.0,
|
||||
"clear_history": false,
|
||||
"data_limit": 500
|
||||
}
|
||||
@ -4,5 +4,5 @@
|
||||
"autoscale": true,
|
||||
"show_magnitude": true,
|
||||
"show_phase": false,
|
||||
"open_air": true
|
||||
"open_air": false
|
||||
}
|
||||
@ -268,4 +268,26 @@
|
||||
.chart-card__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Keyboard shortcuts display */
|
||||
.chart-card__shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-card__shortcuts kbd {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #1e293b;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #cbd5e1 100%);
|
||||
border: 1px solid #94a3b8;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #1e293b inset;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@ -100,14 +100,11 @@ export class ChartManager {
|
||||
height: plotContainer.clientHeight || 420
|
||||
};
|
||||
|
||||
// Disable interactivity for bscan processor
|
||||
// Keep interactivity for bscan processor but disable some features
|
||||
const configOverrides = processorId === 'bscan' ? {
|
||||
staticPlot: false,
|
||||
displayModeBar: false,
|
||||
scrollZoom: false,
|
||||
doubleClick: false,
|
||||
showTips: false,
|
||||
editable: false
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
|
||||
scrollZoom: false
|
||||
} : {};
|
||||
|
||||
createPlotlyPlot(plotContainer, [], layoutOverrides, configOverrides);
|
||||
@ -142,14 +139,11 @@ export class ChartManager {
|
||||
title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } }
|
||||
};
|
||||
|
||||
// Disable interactivity for bscan processor
|
||||
// Keep interactivity for bscan processor but disable some features
|
||||
const configOverrides = processorId === 'bscan' ? {
|
||||
staticPlot: false,
|
||||
displayModeBar: false,
|
||||
scrollZoom: false,
|
||||
doubleClick: false,
|
||||
showTips: false,
|
||||
editable: false
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
|
||||
scrollZoom: false
|
||||
} : {};
|
||||
|
||||
await updatePlotlyPlot(chart.plotContainer, plotlyConfig.data || [], layoutOverrides, configOverrides);
|
||||
@ -163,6 +157,11 @@ export class ChartManager {
|
||||
this.updateChartSettings(processorId);
|
||||
}
|
||||
|
||||
// Clear selection for bscan when data updates
|
||||
if (processorId === 'bscan') {
|
||||
this.bscanClickHandler.onDataUpdate(processorId);
|
||||
}
|
||||
|
||||
const dt = performance.now() - start;
|
||||
this.updatePerformanceStats(dt);
|
||||
});
|
||||
@ -236,6 +235,11 @@ export class ChartManager {
|
||||
<div class="chart-card__meta">
|
||||
<div class="chart-card__timestamp" data-timestamp="">Last update: --</div>
|
||||
<div class="chart-card__sweep" data-sweep=""></div>
|
||||
${processorId === 'bscan' ? `
|
||||
<div class="chart-card__shortcuts" style="font-size: 11px; color: #94a3b8; margin-top: 4px;">
|
||||
Клавиши: <kbd>Клик</kbd> - выбрать | <kbd>D</kbd> - удалить | <kbd>P</kbd> - предпросмотр | <kbd>Esc</kbd> - отмена
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@ -3,11 +3,16 @@
|
||||
* Handles column deletion clicks on the B-Scan heatmap
|
||||
*/
|
||||
|
||||
import { createPlotlyPlot, cleanupPlotly } from '../plotly-utils.js';
|
||||
|
||||
export class BScanClickHandler {
|
||||
constructor(websocket, notifications) {
|
||||
this.websocket = websocket;
|
||||
this.notifications = notifications;
|
||||
this.activeListeners = new Map();
|
||||
this.selectedColumn = null;
|
||||
this.selectedProcessorId = null;
|
||||
this.keyboardListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,13 +38,18 @@ export class BScanClickHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
this.showDeleteConfirmation(processorId, columnIndex);
|
||||
// Select/highlight the column
|
||||
this.selectColumn(processorId, columnIndex, plotContainer);
|
||||
};
|
||||
|
||||
// Attach Plotly click event
|
||||
plotContainer.on('plotly_click', clickHandler);
|
||||
this.activeListeners.set(processorId, clickHandler);
|
||||
this.activeListeners.set(processorId, { clickHandler, plotContainer });
|
||||
|
||||
// Setup keyboard listener if not already done
|
||||
if (!this.keyboardListener) {
|
||||
this.setupKeyboardListener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,11 +58,132 @@ export class BScanClickHandler {
|
||||
* @param {HTMLElement} plotContainer - Plot container element
|
||||
*/
|
||||
detachClickHandler(processorId, plotContainer) {
|
||||
const handler = this.activeListeners.get(processorId);
|
||||
if (handler && plotContainer) {
|
||||
plotContainer.removeListener('plotly_click', handler);
|
||||
const listenerData = this.activeListeners.get(processorId);
|
||||
if (listenerData && plotContainer) {
|
||||
plotContainer.removeListener('plotly_click', listenerData.clickHandler);
|
||||
this.activeListeners.delete(processorId);
|
||||
}
|
||||
|
||||
// Clear selection if this processor was selected
|
||||
if (this.selectedProcessorId === processorId) {
|
||||
this.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select and highlight a column
|
||||
* @param {string} processorId - Processor ID
|
||||
* @param {number} columnIndex - Column index (1-based)
|
||||
* @param {HTMLElement} plotContainer - Plot container element
|
||||
*/
|
||||
selectColumn(processorId, columnIndex, plotContainer) {
|
||||
this.selectedColumn = columnIndex;
|
||||
this.selectedProcessorId = processorId;
|
||||
|
||||
// Add visual highlight by adding a vertical line shape
|
||||
this.highlightColumn(plotContainer, columnIndex);
|
||||
|
||||
console.log(`Column ${columnIndex} selected. Press 'D' to delete.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual highlight to the selected column
|
||||
* @param {HTMLElement} plotContainer - Plot container element
|
||||
* @param {number} columnIndex - Column index (1-based)
|
||||
*/
|
||||
highlightColumn(plotContainer, columnIndex) {
|
||||
if (!plotContainer || typeof Plotly === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a vertical line shape to highlight the column
|
||||
const update = {
|
||||
shapes: [{
|
||||
type: 'line',
|
||||
x0: columnIndex - 0.5,
|
||||
x1: columnIndex - 0.5,
|
||||
y0: 0,
|
||||
y1: 1,
|
||||
yref: 'paper',
|
||||
line: {
|
||||
color: 'rgba(255, 0, 0, 0.8)',
|
||||
width: 3,
|
||||
dash: 'solid'
|
||||
}
|
||||
}, {
|
||||
type: 'line',
|
||||
x0: columnIndex + 0.5,
|
||||
x1: columnIndex + 0.5,
|
||||
y0: 0,
|
||||
y1: 1,
|
||||
yref: 'paper',
|
||||
line: {
|
||||
color: 'rgba(255, 0, 0, 0.8)',
|
||||
width: 3,
|
||||
dash: 'solid'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
Plotly.relayout(plotContainer, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear column selection and highlighting
|
||||
*/
|
||||
clearSelection() {
|
||||
if (!this.selectedProcessorId || !this.selectedColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listenerData = this.activeListeners.get(this.selectedProcessorId);
|
||||
if (listenerData && listenerData.plotContainer && typeof Plotly !== 'undefined') {
|
||||
// Remove shapes (highlighting)
|
||||
Plotly.relayout(listenerData.plotContainer, { shapes: [] });
|
||||
}
|
||||
|
||||
this.selectedColumn = null;
|
||||
this.selectedProcessorId = null;
|
||||
|
||||
console.log('Selection cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup keyboard listener for 'D' key
|
||||
*/
|
||||
setupKeyboardListener() {
|
||||
this.keyboardListener = (event) => {
|
||||
// Ignore if user is typing in an input field
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && (
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.isContentEditable
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if 'D' key is pressed (case insensitive)
|
||||
if (event.key === 'd' || event.key === 'D') {
|
||||
// Delete selected column if any
|
||||
if (this.selectedColumn !== null && this.selectedProcessorId !== null) {
|
||||
event.preventDefault();
|
||||
this.showDeleteConfirmation(this.selectedProcessorId, this.selectedColumn);
|
||||
}
|
||||
} else if (event.key === 'p' || event.key === 'P') {
|
||||
// Show sweep preview modal
|
||||
if (this.selectedColumn !== null && this.selectedProcessorId !== null) {
|
||||
event.preventDefault();
|
||||
this.showSweepPreview(this.selectedProcessorId, this.selectedColumn);
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
// Clear selection on Escape
|
||||
this.clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', this.keyboardListener);
|
||||
console.log('Keyboard shortcuts: D - delete column, P - preview sweep, Escape - deselect');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,7 +205,19 @@ export class BScanClickHandler {
|
||||
* @param {number} columnIndex - Column index to delete (1-based)
|
||||
*/
|
||||
deleteColumn(processorId, columnIndex) {
|
||||
if (!this.websocket || !this.websocket.ws || this.websocket.ws.readyState !== WebSocket.OPEN) {
|
||||
// Get websocket from global window object if not available in instance
|
||||
const websocket = this.websocket || window.vnaDashboard?.websocket;
|
||||
|
||||
console.log('deleteColumn called:', { processorId, columnIndex, websocket, ws: websocket?.ws, readyState: websocket?.ws?.readyState });
|
||||
|
||||
if (!websocket || !websocket.ws || websocket.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket not available or not open:', {
|
||||
hasWebsocket: !!websocket,
|
||||
hasWs: !!websocket?.ws,
|
||||
readyState: websocket?.ws?.readyState,
|
||||
OPEN: WebSocket.OPEN
|
||||
});
|
||||
|
||||
this.notifications?.show?.({
|
||||
type: 'error',
|
||||
title: 'Ошибка подключения',
|
||||
@ -90,7 +233,8 @@ export class BScanClickHandler {
|
||||
column_index: columnIndex
|
||||
};
|
||||
|
||||
this.websocket.ws.send(JSON.stringify(message));
|
||||
console.log('Sending delete column message:', message);
|
||||
websocket.ws.send(JSON.stringify(message));
|
||||
|
||||
this.notifications?.show?.({
|
||||
type: 'info',
|
||||
@ -108,10 +252,324 @@ export class BScanClickHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show sweep preview modal for selected column
|
||||
* @param {string} processorId - Processor ID
|
||||
* @param {number} columnIndex - Column index (1-based)
|
||||
*/
|
||||
showSweepPreview(processorId, columnIndex) {
|
||||
// Get websocket from global window object if not available in instance
|
||||
const websocket = this.websocket || window.vnaDashboard?.websocket;
|
||||
|
||||
if (!websocket || !websocket.ws || websocket.ws.readyState !== WebSocket.OPEN) {
|
||||
this.notifications?.show?.({
|
||||
type: 'error',
|
||||
title: 'Ошибка подключения',
|
||||
message: 'WebSocket не подключен'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Request processor state to get sweep data
|
||||
const message = {
|
||||
type: 'get_processor_state',
|
||||
processor_id: processorId
|
||||
};
|
||||
|
||||
console.log('Requesting processor state for sweep preview:', message);
|
||||
|
||||
// Set up one-time listener for the response
|
||||
const handleResponse = (event) => {
|
||||
try {
|
||||
const response = JSON.parse(event.data);
|
||||
|
||||
if (response.type === 'processor_state' && response.processor_id === processorId) {
|
||||
// Remove this listener
|
||||
websocket.ws.removeEventListener('message', handleResponse);
|
||||
|
||||
// Extract sweep data for the selected column
|
||||
this.displaySweepModal(response, columnIndex);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing processor state response:', error);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.ws.addEventListener('message', handleResponse);
|
||||
websocket.ws.send(JSON.stringify(message));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to request sweep preview:', error);
|
||||
this.notifications?.show?.({
|
||||
type: 'error',
|
||||
title: 'Ошибка предпросмотра',
|
||||
message: 'Не удалось загрузить данные свипа'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display sweep data in modal window
|
||||
* @param {Object} processorState - Processor state from backend
|
||||
* @param {number} columnIndex - Column index (1-based, same as sweep number)
|
||||
*/
|
||||
displaySweepModal(processorState, columnIndex) {
|
||||
const sweepHistory = processorState.state?.sweep_history || [];
|
||||
const config = processorState.state?.config || {};
|
||||
|
||||
// Convert 1-based column index to 0-based array index
|
||||
const sweepIndex = columnIndex - 1;
|
||||
|
||||
if (sweepIndex < 0 || sweepIndex >= sweepHistory.length) {
|
||||
this.notifications?.show?.({
|
||||
type: 'error',
|
||||
title: 'Ошибка',
|
||||
message: `Свип ${columnIndex} не найден в истории`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sweep = sweepHistory[sweepIndex];
|
||||
|
||||
// Use same logic as BScanProcessor: calibrated_data or sweep_data
|
||||
const hasCalibrated = sweep.calibrated_points && sweep.calibrated_points.length > 0;
|
||||
const dataToProcess = hasCalibrated ? sweep.calibrated_points : (sweep.sweep_points || []);
|
||||
const referencePoints = sweep.reference_points || [];
|
||||
const openAirEnabled = config.open_air || false;
|
||||
|
||||
if (dataToProcess.length === 0) {
|
||||
this.notifications?.show?.({
|
||||
type: 'warning',
|
||||
title: 'Нет данных',
|
||||
message: `Свип ${columnIndex} не содержит данных для отображения`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show modal with plot
|
||||
this.showSweepPlotModal(
|
||||
columnIndex,
|
||||
dataToProcess,
|
||||
referencePoints,
|
||||
openAirEnabled,
|
||||
hasCalibrated,
|
||||
sweep.vna_config
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show modal with sweep plot
|
||||
* @param {number} sweepNumber - Sweep number (1-based)
|
||||
* @param {Array} points - Sweep points (calibrated or raw)
|
||||
* @param {Array} referencePoints - Reference points for open air subtraction
|
||||
* @param {boolean} openAirEnabled - Whether to subtract reference
|
||||
* @param {boolean} hasCalibrated - Whether the data is calibrated (true) or raw (false)
|
||||
* @param {Object} vnaConfig - VNA configuration
|
||||
*/
|
||||
showSweepPlotModal(sweepNumber, points, referencePoints, openAirEnabled, hasCalibrated, vnaConfig) {
|
||||
// Get modal element
|
||||
const modal = document.getElementById('plotsModal');
|
||||
if (!modal) {
|
||||
console.error('Plots modal not found');
|
||||
return;
|
||||
}
|
||||
// Prepare data for Plotly
|
||||
const frequencies = [];
|
||||
const magnitudes = [];
|
||||
const phases = [];
|
||||
|
||||
points.forEach((point, index) => {
|
||||
const freq = vnaConfig?.start_freq + (index / (points.length - 1)) * (vnaConfig?.stop_freq - vnaConfig?.start_freq);
|
||||
frequencies.push(freq / 1e6); // Convert to MHz
|
||||
|
||||
let real = point[0] || point.real || point.r || 0;
|
||||
let imag = point[1] || point.imag || point.i || 0;
|
||||
|
||||
// Apply reference subtraction if enabled (same as BScanProcessor)
|
||||
if (openAirEnabled && referencePoints && referencePoints.length > index) {
|
||||
const refPoint = referencePoints[index];
|
||||
const refReal = refPoint[0] || refPoint.real || refPoint.r || 0;
|
||||
const refImag = refPoint[1] || refPoint.imag || refPoint.i || 0;
|
||||
|
||||
// Subtract reference: complex_data - reference_complex
|
||||
real = real - refReal;
|
||||
imag = imag - refImag;
|
||||
}
|
||||
|
||||
const magnitude = Math.sqrt(real * real + imag * imag);
|
||||
const phase = Math.atan2(imag, real) * (180 / Math.PI);
|
||||
|
||||
magnitudes.push(magnitude);
|
||||
phases.push(phase);
|
||||
});
|
||||
|
||||
// Create Plotly traces
|
||||
const traces = [
|
||||
{
|
||||
x: frequencies,
|
||||
y: magnitudes,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Амплитуда',
|
||||
line: { color: '#3b82f6', width: 2 }
|
||||
},
|
||||
{
|
||||
x: frequencies,
|
||||
y: phases,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Фаза',
|
||||
yaxis: 'y2',
|
||||
line: { color: '#f59e0b', width: 2 }
|
||||
}
|
||||
];
|
||||
|
||||
// Build descriptive title
|
||||
const dataType = hasCalibrated ? 'откалиброванные' : 'сырые';
|
||||
const refStatus = openAirEnabled && referencePoints && referencePoints.length > 0 ? 'с вычетом референса' : 'без вычета референса';
|
||||
const titleText = `Свип ${sweepNumber} (${dataType}, ${refStatus})`;
|
||||
|
||||
const layoutOverrides = {
|
||||
title: { text: titleText, font: { size: 16, color: '#f1f5f9' } },
|
||||
xaxis: {
|
||||
title: 'Частота (МГц)',
|
||||
gridcolor: '#334155',
|
||||
zerolinecolor: '#475569',
|
||||
color: '#cbd5e1',
|
||||
fixedrange: false
|
||||
},
|
||||
yaxis: {
|
||||
title: 'Амплитуда',
|
||||
side: 'left',
|
||||
gridcolor: '#334155',
|
||||
zerolinecolor: '#475569',
|
||||
color: '#cbd5e1',
|
||||
fixedrange: false
|
||||
},
|
||||
yaxis2: {
|
||||
title: 'Фаза (°)',
|
||||
overlaying: 'y',
|
||||
side: 'right',
|
||||
gridcolor: '#334155',
|
||||
zerolinecolor: '#475569',
|
||||
color: '#cbd5e1',
|
||||
fixedrange: false
|
||||
},
|
||||
showlegend: true,
|
||||
legend: { x: 0.01, y: 0.99 },
|
||||
height: 500
|
||||
};
|
||||
|
||||
const configOverrides = {
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: `sweep-${sweepNumber}-preview`,
|
||||
height: 600,
|
||||
width: 800,
|
||||
scale: 1
|
||||
}
|
||||
};
|
||||
|
||||
// Update modal title
|
||||
const title = modal.querySelector('.modal__title');
|
||||
if (title) {
|
||||
title.textContent = titleText;
|
||||
}
|
||||
|
||||
// Get plots grid container and clear it
|
||||
const container = document.getElementById('plotsGrid');
|
||||
if (!container) {
|
||||
console.error('Plots grid container not found');
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create card wrapper with proper styling
|
||||
const card = document.createElement('div');
|
||||
card.className = 'chart-card';
|
||||
card.innerHTML = `
|
||||
<div class="chart-card__header">
|
||||
<div class="chart-card__title">${titleText}</div>
|
||||
</div>
|
||||
<div class="chart-card__content">
|
||||
<div class="chart-card__plot" id="sweep-preview-plot"></div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
|
||||
// Setup close handlers
|
||||
this.setupModalCloseHandlers(modal);
|
||||
|
||||
// Show modal
|
||||
modal.classList.add('modal--active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Create plot after modal is shown
|
||||
setTimeout(() => {
|
||||
const plotElement = document.getElementById('sweep-preview-plot');
|
||||
if (plotElement) {
|
||||
createPlotlyPlot(plotElement, traces, layoutOverrides, configOverrides);
|
||||
} else {
|
||||
console.error('Plot element not found');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup modal close handlers
|
||||
* @param {HTMLElement} modal - Modal element
|
||||
*/
|
||||
setupModalCloseHandlers(modal) {
|
||||
const closeElements = modal.querySelectorAll('[data-modal-close]');
|
||||
closeElements.forEach(el => {
|
||||
el.onclick = () => this.closeModal(modal);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal
|
||||
* @param {HTMLElement} modal - Modal element
|
||||
*/
|
||||
closeModal(modal) {
|
||||
modal.classList.remove('modal--active');
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Cleanup Plotly plot
|
||||
const plotElement = document.getElementById('sweep-preview-plot');
|
||||
if (plotElement) {
|
||||
cleanupPlotly(plotElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when data is updated (new columns added, imported, etc.)
|
||||
* Clears the current selection
|
||||
* @param {string} processorId - Processor ID
|
||||
*/
|
||||
onDataUpdate(processorId) {
|
||||
// Clear selection if this processor's data was updated
|
||||
if (this.selectedProcessorId === processorId) {
|
||||
this.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all listeners
|
||||
*/
|
||||
destroy() {
|
||||
// Remove keyboard listener
|
||||
if (this.keyboardListener) {
|
||||
document.removeEventListener('keydown', this.keyboardListener);
|
||||
this.keyboardListener = null;
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
this.clearSelection();
|
||||
|
||||
// Clear active listeners
|
||||
this.activeListeners.clear();
|
||||
|
||||
console.log('BScanClickHandler destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user