Click and drag on empty space to draw a selection rectangle. Gates inside the box get selected (cyan dashed outline). Drag any selected gate to move all of them together. Delete/Backspace removes all selected gates and their connections. Escape clears the selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
584 lines
23 KiB
JavaScript
584 lines
23 KiB
JavaScript
// Event handlers — mouse, keyboard, toolbar, waveform controls
|
|
import { GATE_W, GATE_H, COMP_W } from './constants.js';
|
|
import { state } from './state.js';
|
|
import { evaluateAll, findGateAt, findPortAt, getComponentWidth, getComponentHeight } from './gates.js';
|
|
import { manualStep, clearWaveData } from './waveform.js';
|
|
import { startSim, stopSim, adjustSpeed } from './simulation.js';
|
|
import { resize, screenToWorld } from './renderer.js';
|
|
import { puzzleMode, currentLevel, showLevelPanel } from './puzzleUI.js';
|
|
import { getLevel } from './levels.js';
|
|
import { saveState, loadState, exportAsFile, importFromFile } from './saveLoad.js';
|
|
import { enterComponentEditor, editComponentBlueprint, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
|
|
import { getExampleList, loadExample } from './examples.js';
|
|
import { createBusFromCut } from './bus.js';
|
|
|
|
const PAN_SPEED = 40;
|
|
|
|
function updateWaveZoomLabel() {
|
|
const el = document.getElementById('wave-zoom-label');
|
|
if (el) el.textContent = `${state.waveZoom}px`;
|
|
}
|
|
|
|
export function initEvents() {
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
// Set up resize callback for component editor
|
|
setResizeCallback(resize);
|
|
|
|
// ==================== CANVAS MOUSE ====================
|
|
canvas.addEventListener('mousemove', e => {
|
|
state.mouseX = e.offsetX;
|
|
state.mouseY = e.offsetY;
|
|
|
|
// Convert to world coords for gate/port detection
|
|
const world = screenToWorld(e.offsetX, e.offsetY);
|
|
|
|
// Update bus cut line endpoint
|
|
if (state.busCutting) {
|
|
state.busCutting.endX = world.x;
|
|
state.busCutting.endY = world.y;
|
|
return;
|
|
}
|
|
|
|
// Update selection box
|
|
if (state.selectionBox) {
|
|
state.selectionBox.endX = world.x;
|
|
state.selectionBox.endY = world.y;
|
|
return;
|
|
}
|
|
|
|
// Multi-drag selected gates
|
|
if (state.multiDrag) {
|
|
if (dragStartPos && !dragMoved) {
|
|
const dx = e.offsetX - dragStartPos.x;
|
|
const dy = e.offsetY - dragStartPos.y;
|
|
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragMoved = true;
|
|
}
|
|
if (dragMoved) {
|
|
const dx = world.x - state.multiDrag.startX;
|
|
const dy = world.y - state.multiDrag.startY;
|
|
for (const orig of state.multiDrag.origins) {
|
|
const gate = state.gates.find(g => g.id === orig.id);
|
|
if (gate) {
|
|
gate.x = orig.x + dx;
|
|
gate.y = orig.y + dy;
|
|
}
|
|
}
|
|
evaluateAll();
|
|
}
|
|
return;
|
|
}
|
|
|
|
state.hoveredPort = findPortAt(world.x, world.y);
|
|
state.hoveredGate = state.hoveredPort ? state.hoveredPort.gate : findGateAt(world.x, world.y);
|
|
|
|
if (state.dragging) {
|
|
// Detect if mouse actually moved (threshold of 4px to distinguish click vs drag)
|
|
if (dragStartPos && !dragMoved) {
|
|
const dx = e.offsetX - dragStartPos.x;
|
|
const dy = e.offsetY - dragStartPos.y;
|
|
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
|
|
dragMoved = true;
|
|
}
|
|
}
|
|
if (dragMoved) {
|
|
state.dragging.x = world.x - state.dragOffset.x;
|
|
state.dragging.y = world.y - state.dragOffset.y;
|
|
evaluateAll();
|
|
}
|
|
}
|
|
|
|
canvas.style.cursor = state.placingGate ? 'crosshair'
|
|
: state.selectionBox ? 'crosshair'
|
|
: state.hoveredPort ? 'pointer'
|
|
: state.hoveredGate ? 'grab'
|
|
: 'default';
|
|
});
|
|
|
|
let dragStartPos = null;
|
|
let dragMoved = false;
|
|
|
|
canvas.addEventListener('mousedown', e => {
|
|
if (e.button !== 0) return;
|
|
const world = screenToWorld(e.offsetX, e.offsetY);
|
|
dragStartPos = { x: e.offsetX, y: e.offsetY };
|
|
dragMoved = false;
|
|
|
|
// Shift+click on empty space → start bus cut
|
|
if (e.shiftKey && !state.placingGate) {
|
|
const port = findPortAt(world.x, world.y);
|
|
const gate = findGateAt(world.x, world.y);
|
|
if (!port && !gate) {
|
|
state.busCutting = {
|
|
startX: world.x, startY: world.y,
|
|
endX: world.x, endY: world.y
|
|
};
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Placing a new gate
|
|
if (state.placingGate) {
|
|
let w = GATE_W, h = GATE_H;
|
|
if (state.placingGate.startsWith('COMPONENT:')) {
|
|
const componentId = state.placingGate.substring(10);
|
|
const component = state.customComponents?.[componentId];
|
|
if (component) {
|
|
const fakeGate = { type: state.placingGate, component };
|
|
w = getComponentWidth(fakeGate);
|
|
h = getComponentHeight(fakeGate);
|
|
}
|
|
}
|
|
|
|
const newGate = {
|
|
id: state.nextId++,
|
|
type: state.placingGate,
|
|
x: world.x - w / 2,
|
|
y: world.y - h / 2,
|
|
value: 0
|
|
};
|
|
if (state.placingGate.startsWith('COMPONENT:')) {
|
|
newGate.component = state.customComponents[state.placingGate.substring(10)];
|
|
}
|
|
state.gates.push(newGate);
|
|
evaluateAll();
|
|
// Keep placingGate active so user can place multiple — right-click to cancel
|
|
return;
|
|
}
|
|
|
|
// Port click — connecting
|
|
const port = findPortAt(world.x, world.y);
|
|
if (port) {
|
|
if (state.connecting) {
|
|
if (state.connecting.portType === 'output' && port.type === 'input') {
|
|
state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
|
const conn = { from: state.connecting.gate.id, fromPort: state.connecting.portIndex, to: port.gate.id, toPort: port.index };
|
|
state.connections.push(conn);
|
|
console.log(`[wire] ${conn.from}:${conn.fromPort} → ${conn.to}:${conn.toPort}`);
|
|
evaluateAll();
|
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
|
} else if (state.connecting.portType === 'input' && port.type === 'output') {
|
|
state.connections = state.connections.filter(c => !(c.to === state.connecting.gate.id && c.toPort === state.connecting.portIndex));
|
|
const conn = { from: port.gate.id, fromPort: port.index, to: state.connecting.gate.id, toPort: state.connecting.portIndex };
|
|
state.connections.push(conn);
|
|
console.log(`[wire] ${conn.from}:${conn.fromPort} → ${conn.to}:${conn.toPort}`);
|
|
evaluateAll();
|
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
|
}
|
|
state.connecting = null;
|
|
} else {
|
|
state.connecting = { gate: port.gate, portIndex: port.index, portType: port.type };
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state.connecting) { state.connecting = null; return; }
|
|
|
|
// Drag any gate (including INPUT/CLOCK)
|
|
const gate = findGateAt(world.x, world.y);
|
|
if (gate) {
|
|
// If clicking a selected gate → multi-drag all selected
|
|
if (state.selectedGates.includes(gate.id)) {
|
|
state.multiDrag = {
|
|
startX: world.x,
|
|
startY: world.y,
|
|
origins: state.selectedGates.map(id => {
|
|
const g = state.gates.find(g => g.id === id);
|
|
return g ? { id: g.id, x: g.x, y: g.y } : null;
|
|
}).filter(Boolean)
|
|
};
|
|
canvas.style.cursor = 'grabbing';
|
|
return;
|
|
}
|
|
// Clicking an unselected gate → clear selection, drag just this one
|
|
state.selectedGates = [];
|
|
state.dragging = gate;
|
|
state.dragOffset = { x: world.x - gate.x, y: world.y - gate.y };
|
|
canvas.style.cursor = 'grabbing';
|
|
return;
|
|
}
|
|
|
|
// Click on empty space → clear selection and start selection box
|
|
state.selectedGates = [];
|
|
state.selectionBox = {
|
|
startX: world.x, startY: world.y,
|
|
endX: world.x, endY: world.y
|
|
};
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', e => {
|
|
// Finish bus cut
|
|
if (state.busCutting) {
|
|
createBusFromCut();
|
|
state.busCutting = null;
|
|
return;
|
|
}
|
|
|
|
// Finish selection box → select gates inside
|
|
if (state.selectionBox) {
|
|
const box = state.selectionBox;
|
|
const x1 = Math.min(box.startX, box.endX);
|
|
const y1 = Math.min(box.startY, box.endY);
|
|
const x2 = Math.max(box.startX, box.endX);
|
|
const y2 = Math.max(box.startY, box.endY);
|
|
|
|
// Only select if box is big enough (not just a click)
|
|
if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
|
|
state.selectedGates = state.gates
|
|
.filter(g => {
|
|
const isDynamic = g.type.startsWith('COMPONENT:') || g.type.startsWith('BUS:');
|
|
const gw = isDynamic ? getComponentWidth(g) : GATE_W;
|
|
const gh = isDynamic ? getComponentHeight(g) : GATE_H;
|
|
// Gate overlaps selection box
|
|
return g.x + gw > x1 && g.x < x2 && g.y + gh > y1 && g.y < y2;
|
|
})
|
|
.map(g => g.id);
|
|
if (state.selectedGates.length > 0) {
|
|
console.log(`[select] ${state.selectedGates.length} gate(s) selected`);
|
|
}
|
|
}
|
|
state.selectionBox = null;
|
|
dragStartPos = null;
|
|
return;
|
|
}
|
|
|
|
// Finish multi-drag
|
|
if (state.multiDrag) {
|
|
state.multiDrag = null;
|
|
dragStartPos = null;
|
|
return;
|
|
}
|
|
|
|
// Toggle INPUT/CLOCK only on click (no drag movement)
|
|
if (state.dragging && !dragMoved) {
|
|
const gate = state.dragging;
|
|
if (gate.type === 'INPUT' || gate.type === 'CLOCK') {
|
|
gate.value = gate.value ? 0 : 1;
|
|
console.log(`[toggle] ${gate.type}#${gate.id} → ${gate.value}`);
|
|
evaluateAll(true); // record waveform on intentional toggle
|
|
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
|
}
|
|
}
|
|
state.dragging = null;
|
|
dragStartPos = null;
|
|
});
|
|
|
|
// Double-click to rename INPUT/OUTPUT/CLOCK gates, or edit component blueprint
|
|
canvas.addEventListener('dblclick', e => {
|
|
const world = screenToWorld(e.offsetX, e.offsetY);
|
|
const gate = findGateAt(world.x, world.y);
|
|
if (!gate) return;
|
|
|
|
// Double-click on component gate → edit its blueprint
|
|
if (gate.type.startsWith('COMPONENT:') && gate.component) {
|
|
editComponentBlueprint(gate);
|
|
return;
|
|
}
|
|
|
|
// Double-click on I/O gates → rename (only inside component editor)
|
|
if (state.componentEditorActive && (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK')) {
|
|
const current = gate.label || '';
|
|
const label = prompt(`Label for ${gate.type}#${gate.id}:`, current);
|
|
if (label !== null) {
|
|
gate.label = label.trim() || undefined;
|
|
console.log(`[label] ${gate.type}#${gate.id} → "${gate.label || ''}"`);
|
|
}
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('contextmenu', e => {
|
|
e.preventDefault();
|
|
|
|
// Right-click cancels placing mode
|
|
if (state.placingGate) {
|
|
state.placingGate = null;
|
|
return;
|
|
}
|
|
|
|
const world = screenToWorld(e.offsetX, e.offsetY);
|
|
const port = findPortAt(world.x, world.y);
|
|
if (port && port.type === 'input') {
|
|
state.connections = state.connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
|
evaluateAll();
|
|
} else if (port && port.type === 'output') {
|
|
state.connections = state.connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
|
|
evaluateAll();
|
|
}
|
|
});
|
|
|
|
// ==================== MOUSE WHEEL ZOOM ====================
|
|
canvas.addEventListener('wheel', e => {
|
|
e.preventDefault();
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
const newZoom = Math.max(0.2, Math.min(3, state.zoom + delta));
|
|
|
|
// Zoom towards mouse position
|
|
const worldBefore = screenToWorld(e.offsetX, e.offsetY);
|
|
state.zoom = newZoom;
|
|
const worldAfter = screenToWorld(e.offsetX, e.offsetY);
|
|
|
|
state.camX += (worldAfter.x - worldBefore.x) * state.zoom;
|
|
state.camY += (worldAfter.y - worldBefore.y) * state.zoom;
|
|
}, { passive: false });
|
|
|
|
// ==================== KEYBOARD ====================
|
|
const keysDown = new Set();
|
|
|
|
document.addEventListener('keydown', e => {
|
|
keysDown.add(e.key);
|
|
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
if (document.activeElement !== document.body) return;
|
|
e.preventDefault();
|
|
|
|
// Delete all selected gates
|
|
if (state.selectedGates.length > 0) {
|
|
for (const gateId of state.selectedGates) {
|
|
state.connections = state.connections.filter(c => c.from !== gateId && c.to !== gateId);
|
|
state.gates = state.gates.filter(g => g.id !== gateId);
|
|
delete state.waveData[gateId];
|
|
}
|
|
console.log(`[delete] removed ${state.selectedGates.length} gate(s)`);
|
|
state.selectedGates = [];
|
|
state.hoveredGate = null;
|
|
evaluateAll();
|
|
} else if (state.hoveredGate) {
|
|
// Delete single hovered gate
|
|
const gateId = state.hoveredGate.id;
|
|
state.connections = state.connections.filter(c => c.from !== gateId && c.to !== gateId);
|
|
state.gates = state.gates.filter(g => g.id !== gateId);
|
|
delete state.waveData[gateId];
|
|
state.hoveredGate = null;
|
|
evaluateAll();
|
|
}
|
|
}
|
|
if (e.key === 'Escape') {
|
|
state.placingGate = null;
|
|
state.connecting = null;
|
|
state.selectedGates = [];
|
|
}
|
|
|
|
// Pan with arrow keys
|
|
if (e.key === 'ArrowLeft') { state.camX += PAN_SPEED; e.preventDefault(); }
|
|
if (e.key === 'ArrowRight') { state.camX -= PAN_SPEED; e.preventDefault(); }
|
|
if (e.key === 'ArrowUp') { state.camY += PAN_SPEED; e.preventDefault(); }
|
|
if (e.key === 'ArrowDown') { state.camY -= PAN_SPEED; e.preventDefault(); }
|
|
|
|
// Zoom with +/- keys
|
|
if ((e.key === '+' || e.key === '=') && document.activeElement === document.body) {
|
|
state.zoom = Math.min(3, state.zoom + 0.1);
|
|
e.preventDefault();
|
|
}
|
|
if ((e.key === '-' || e.key === '_') && document.activeElement === document.body) {
|
|
state.zoom = Math.max(0.2, state.zoom - 0.1);
|
|
e.preventDefault();
|
|
}
|
|
|
|
// Reset view with 0
|
|
if (e.key === '0' && document.activeElement === document.body) {
|
|
state.camX = 0;
|
|
state.camY = 0;
|
|
state.zoom = 1;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', e => {
|
|
keysDown.delete(e.key);
|
|
});
|
|
|
|
// ==================== TOOLBAR DROPDOWNS ====================
|
|
function closeAllDropdowns() {
|
|
document.querySelectorAll('.toolbar-dropdown.open').forEach(d => d.classList.remove('open'));
|
|
}
|
|
|
|
document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
|
|
toggle.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const dropdown = toggle.parentElement;
|
|
const wasOpen = dropdown.classList.contains('open');
|
|
closeAllDropdowns();
|
|
if (!wasOpen) dropdown.classList.add('open');
|
|
});
|
|
});
|
|
|
|
// Close dropdowns when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.toolbar-dropdown')) {
|
|
closeAllDropdowns();
|
|
}
|
|
});
|
|
|
|
// Gate buttons inside dropdowns
|
|
document.querySelectorAll('.dropdown-menu .gate-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const gateName = btn.dataset.gate;
|
|
// In puzzle mode, check if gate is allowed
|
|
if (puzzleMode && currentLevel) {
|
|
const level = getLevel(currentLevel.id);
|
|
if (!level.availableGates.includes(gateName)) {
|
|
alert(`${gateName} is not available in this level.`);
|
|
return;
|
|
}
|
|
}
|
|
state.placingGate = gateName;
|
|
closeAllDropdowns();
|
|
});
|
|
});
|
|
|
|
// ==================== EXAMPLES ====================
|
|
const examplesMenu = document.getElementById('examples-menu');
|
|
getExampleList().forEach((ex, i) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'example-btn';
|
|
btn.innerHTML = `<span class="example-name">${ex.name}</span><span class="example-desc">${ex.description}</span>`;
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const hasGates = state.gates.length > 0;
|
|
if (hasGates && !confirm('Load example? This will replace your current circuit.')) return;
|
|
const data = loadExample(i);
|
|
if (data) {
|
|
state.gates = data.circuit.gates;
|
|
state.connections = data.circuit.connections;
|
|
state.nextId = data.circuit.nextId;
|
|
if (data.camera) {
|
|
state.camX = data.camera.camX;
|
|
state.camY = data.camera.camY;
|
|
state.zoom = data.camera.zoom;
|
|
}
|
|
clearWaveData();
|
|
evaluateAll();
|
|
}
|
|
closeAllDropdowns();
|
|
});
|
|
examplesMenu.appendChild(btn);
|
|
});
|
|
|
|
document.getElementById('clear-btn').addEventListener('click', () => {
|
|
if (state.gates.length === 0 || confirm('Clear all gates and connections?')) {
|
|
state.gates = [];
|
|
state.connections = [];
|
|
state.connecting = null;
|
|
state.placingGate = null;
|
|
clearWaveData();
|
|
}
|
|
});
|
|
|
|
// Export/Import
|
|
document.getElementById('export-btn').addEventListener('click', () => {
|
|
exportAsFile(`circuit-${new Date().toISOString().slice(0, 10)}.json`);
|
|
});
|
|
|
|
document.getElementById('import-btn').addEventListener('click', () => {
|
|
document.getElementById('import-file').click();
|
|
});
|
|
|
|
document.getElementById('import-file').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
try {
|
|
const result = await importFromFile(file);
|
|
if (result.success) {
|
|
alert('Circuit imported successfully!');
|
|
evaluateAll();
|
|
} else {
|
|
alert(`Import failed: ${result.error}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Import error: ${err.message}`);
|
|
}
|
|
// Reset file input
|
|
e.target.value = '';
|
|
});
|
|
|
|
// Help modal
|
|
document.getElementById('help-btn').addEventListener('click', () => {
|
|
document.getElementById('help-modal').classList.add('visible');
|
|
});
|
|
document.getElementById('help-close').addEventListener('click', () => {
|
|
document.getElementById('help-modal').classList.remove('visible');
|
|
});
|
|
document.getElementById('help-modal').addEventListener('click', e => {
|
|
if (e.target === e.currentTarget) document.getElementById('help-modal').classList.remove('visible');
|
|
});
|
|
|
|
// Waveform toggle
|
|
document.getElementById('sim-btn').addEventListener('click', () => {
|
|
state.waveformVisible = !state.waveformVisible;
|
|
document.getElementById('waveform-panel').classList.toggle('visible', state.waveformVisible);
|
|
document.getElementById('sim-btn').classList.toggle('active', state.waveformVisible);
|
|
resize();
|
|
});
|
|
|
|
// ==================== WAVEFORM CONTROLS ====================
|
|
document.getElementById('wave-record').addEventListener('click', function() {
|
|
state.recording = !state.recording;
|
|
this.classList.toggle('active', state.recording);
|
|
this.textContent = state.recording ? 'Record' : 'Paused';
|
|
});
|
|
|
|
document.getElementById('wave-clear').addEventListener('click', clearWaveData);
|
|
document.getElementById('wave-step').addEventListener('click', manualStep);
|
|
|
|
document.getElementById('wave-zoom-in').addEventListener('click', () => {
|
|
state.waveZoom = Math.min(100, Math.round(state.waveZoom * 1.5));
|
|
updateWaveZoomLabel();
|
|
});
|
|
document.getElementById('wave-zoom-out').addEventListener('click', () => {
|
|
state.waveZoom = Math.max(2, Math.round(state.waveZoom / 1.5));
|
|
updateWaveZoomLabel();
|
|
});
|
|
|
|
// Init zoom label
|
|
updateWaveZoomLabel();
|
|
|
|
// ==================== SIMULATION CONTROLS ====================
|
|
document.getElementById('sim-run-btn').addEventListener('click', () => {
|
|
if (state.simRunning) stopSim(); else startSim();
|
|
});
|
|
document.getElementById('sim-faster').addEventListener('click', () => adjustSpeed(-100));
|
|
document.getElementById('sim-slower').addEventListener('click', () => adjustSpeed(100));
|
|
|
|
// ==================== WAVEFORM PANEL RESIZE ====================
|
|
const resizeHandle = document.getElementById('wave-resize');
|
|
resizeHandle.addEventListener('mousedown', e => {
|
|
state.resizingWave = true;
|
|
e.preventDefault();
|
|
});
|
|
document.addEventListener('mousemove', e => {
|
|
if (state.resizingWave) {
|
|
state.waveformHeight = Math.max(100, Math.min(500, window.innerHeight - e.clientY));
|
|
document.getElementById('waveform-panel').style.height = state.waveformHeight + 'px';
|
|
resize();
|
|
}
|
|
});
|
|
document.addEventListener('mouseup', () => { state.resizingWave = false; });
|
|
|
|
// ==================== COMPONENT EDITOR ====================
|
|
document.getElementById('create-component-btn').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
closeAllDropdowns();
|
|
enterComponentEditor();
|
|
});
|
|
|
|
document.getElementById('component-editor-save').addEventListener('click', () => {
|
|
// If editing existing, pre-fill with current name
|
|
const existingName = state.editingComponentId
|
|
? (state.customComponents[state.editingComponentId]?.name || 'MyComponent')
|
|
: 'MyComponent';
|
|
const name = prompt('Component name:', existingName);
|
|
if (name && name.trim()) {
|
|
exitComponentEditor(name.trim(), true);
|
|
}
|
|
});
|
|
|
|
document.getElementById('component-editor-cancel').addEventListener('click', () => {
|
|
if (confirm('Discard component without saving?')) {
|
|
exitComponentEditor('', false);
|
|
}
|
|
});
|
|
|
|
// Update component buttons initially
|
|
updateComponentButtons();
|
|
}
|