1/** 2 * Circuit Drag Handler - Manages drag and drop interactions for circuit gates 3 */ 4class CircuitDragHandler { 5 constructor(composer) { 6 this.composer = composer; 7 this.isDragging = false; 8 this.dragElement = null; 9 this.dragGateType = null; 10 this.dragStartPos = { x: 0, y: 0 }; 11 this.currentHoverCell = null; 12 // When dragging a placed gate, stores { qubit, time } of the source cell. 13 // null means the drag originated from the toolbar (new placement). 14 this.dragSourceGate = null; 15 // DOM element whose opacity is dimmed while its gate is being dragged. 16 this._dragSourceElement = null; 17 18 this.initDragHandlers(); 19 } 20 21 /** 22 * Initialize drag and drop event handlers 23 */ 24 initDragHandlers() { 25 // Gate toolbar drag events 26 document.addEventListener('mousedown', (e) => this.handleMouseDown(e)); 27 document.addEventListener('mousemove', (e) => this.handleMouseMove(e)); 28 document.addEventListener('mouseup', (e) => this.handleMouseUp(e)); 29 30 // Touch events for mobile. 31 // touchmove MUST be non-passive so preventDefault() can suppress scroll 32 // while a gate drag is in progress. touchstart/touchend are fine as 33 // passive: false too since startDrag already calls e.preventDefault(). 34 document.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: false }); 35 document.addEventListener('touchmove', (e) => this.handleTouchMove(e), { passive: false }); 36 document.addEventListener('touchend', (e) => this.handleTouchEnd(e)); 37 38 // Prevent default drag behavior on images and other elements 39 document.addEventListener('dragstart', (e) => e.preventDefault()); 40 } 41 42 /** 43 * Handle mouse down event. 44 * Checks placed gates first so they take priority over toolbar buttons 45 * in the (unlikely) case of overlap. 46 */ 47 handleMouseDown(e) { 48 // --- Drag a gate that's already placed on the circuit grid --- 49 const gateElement = e.target.closest('.gate-element'); 50 if (gateElement) { 51 this.startDragFromGrid(e, gateElement); 52 return; 53 } 54 55 // --- Drag a gate from the toolbar (original behavior) --- 56 const gateButton = e.target.closest('.gate-tool-btn'); 57 if (!gateButton) return; 58 59 const gateType = gateButton.getAttribute('data-gate'); 60 if (!gateType) return; 61 62 this.startDrag(e, gateType, gateButton); 63 } 64 65 /** 66 * Handle touch start event. 67 */ 68 handleTouchStart(e) { 69 // --- Drag a placed gate via touch --- 70 const gateElement = e.target.closest('.gate-element'); 71 if (gateElement) { 72 const touch = e.touches[0]; 73 this.startDragFromGrid(touch, gateElement); 74 return; 75 } 76 77 // --- Drag from toolbar via touch (original behavior) --- 78 const gateButton = e.target.closest('.gate-tool-btn'); 79 if (!gateButton) return; 80 81 const gateType = gateButton.getAttribute('data-gate'); 82 if (!gateType) return; 83 84 const touch = e.touches[0]; 85 this.startDrag(touch, gateType, gateButton); 86 } 87 88 /** 89 * Start drag operation 90 */ 91 startDrag(e, gateType, sourceElement) { 92 e.preventDefault(); 93 94 this.isDragging = true; 95 this.dragGateType = gateType; 96 this.dragStartPos = { x: e.clientX, y: e.clientY }; 97 98 // Create drag element 99 this.createDragElement(gateType, sourceElement); 100 101 // Add dragging class to source 102 sourceElement.classList.add('dragging'); 103 104 // Add global dragging class 105 document.body.classList.add('circuit-dragging'); 106 } 107 108 /** 109 * Start a drag from a gate already placed on the circuit grid. 110 * Records the source cell so endDrag can decide: move vs. delete. 111 */ 112 startDragFromGrid(e, gateElement) { 113 e.preventDefault(); 114 115 const qubit = parseInt(gateElement.getAttribute('data-qubit')); 116 const time = parseInt(gateElement.getAttribute('data-time')); 117 const gateType = gateElement.getAttribute('data-gate-type'); 118 if (isNaN(qubit) || isNaN(time) || !gateType) return; 119 120 this.isDragging = true; 121 this.dragGateType = gateType; 122 this.dragSourceGate = { qubit, time }; 123 this.dragStartPos = { x: e.clientX, y: e.clientY }; 124 125 // Dim the source element in-place so the user can see the "hole" 126 this._dragSourceElement = gateElement; 127 gateElement.style.opacity = '0.25'; 128 gateElement.style.pointerEvents = 'none'; 129 130 this.createDragElement(gateType, gateElement); 131 132 document.body.classList.add('circuit-dragging'); 133 } 134 135 /** 136 * Create drag element 137 */ 138 createDragElement(gateType, sourceElement) { 139 this.dragElement = document.createElement('div'); 140 this.dragElement.className = 'drag-element'; 141 this.dragElement.textContent = gateType; 142 143 // Copy styling from source element 144 const rect = sourceElement.getBoundingClientRect(); 145 this.dragElement.style.width = rect.width + 'px'; 146 this.dragElement.style.height = rect.height + 'px'; 147 this.dragElement.style.position = 'fixed'; 148 this.dragElement.style.zIndex = '10000'; 149 this.dragElement.style.pointerEvents = 'none'; 150 this.dragElement.style.opacity = '0.8'; 151 this.dragElement.style.transform = 'translate(-50%, -50%)'; 152 153 // Apply gate-specific styling 154 const definition = this.composer.gates.getGateDefinition(gateType); 155 this.dragElement.style.color = definition.color; 156 this.dragElement.style.backgroundColor = definition.backgroundColor; 157 this.dragElement.style.border = `2px solid ${definition.borderColor}`; 158 this.dragElement.style.borderRadius = '4px'; 159 this.dragElement.style.display = 'flex'; 160 this.dragElement.style.alignItems = 'center'; 161 this.dragElement.style.justifyContent = 'center'; 162 this.dragElement.style.fontWeight = 'bold'; 163 this.dragElement.style.fontSize = '14px'; 164 165 document.body.appendChild(this.dragElement); 166 } 167 168 /** 169 * Handle mouse move event 170 */ 171 handleMouseMove(e) { 172 if (!this.isDragging || !this.dragElement) return; 173 174 this.updateDragPosition(e.clientX, e.clientY); 175 this.updateHoverCell(e.clientX, e.clientY); 176 } 177 178 /** 179 * Handle touch move event. 180 * preventDefault() is required here: without it the browser interprets 181 * the finger movement as a scroll gesture, causing the page to scroll 182 * while the gate ghost follows the finger. The listener is registered 183 * on document so it must be explicitly non-passive to allow this call. 184 */ 185 handleTouchMove(e) { 186 if (!this.isDragging || !this.dragElement) return; 187 e.preventDefault(); // Stop scroll-during-drag on mobile 188 const touch = e.touches[0]; 189 this.updateDragPosition(touch.clientX, touch.clientY); 190 this.updateHoverCell(touch.clientX, touch.clientY); 191 } 192 193 /** 194 * Update drag element position 195 */ 196 updateDragPosition(x, y) { 197 if (!this.dragElement) return; 198 199 this.dragElement.style.left = x + 'px'; 200 this.dragElement.style.top = y + 'px'; 201 } 202 203 /** 204 * Update hover cell for drop feedback. 205 * When a placed gate is dragged outside the circuit area the drag ghost 206 * turns red to signal "release here = delete". 207 */ 208 updateHoverCell(x, y) { 209 const cell = this.getCellAtPosition(x, y); 210 211 // Remove previous highlight 212 if (this.currentHoverCell) { 213 this.composer.grid.removeHighlight( 214 this.currentHoverCell.qubit, 215 this.currentHoverCell.time 216 ); 217 } 218 219 if (cell) { 220 this.composer.grid.highlightCell(cell.qubit, cell.time); 221 this.currentHoverCell = cell; 222 // Back inside valid drop zone â clear delete indicator 223 if (this.dragElement) { 224 this.dragElement.classList.remove('drag-will-delete'); 225 } 226 } else { 227 this.currentHoverCell = null; 228 // Outside circuit â show delete cue only when moving a placed gate 229 if (this.dragElement && this.dragSourceGate) { 230 this.dragElement.classList.add('drag-will-delete'); 231 } 232 } 233 } 234 235 /** 236 * Handle mouse up event 237 */ 238 handleMouseUp(e) { 239 if (!this.isDragging) return; 240 241 this.endDrag(e.clientX, e.clientY); 242 } 243 244 /** 245 * Handle touch end event 246 */ 247 handleTouchEnd(e) { 248 if (!this.isDragging) return; 249 250 const touch = e.changedTouches[0]; 251 this.endDrag(touch.clientX, touch.clientY); 252 } 253 254 /** 255 * End drag operation. 256 *
257 * Three cases depending on drag origin: 258 * 259 * A) Drag from toolbar â drop on grid cell: place new gate (existing behaviour). 260 * B) Drag from placed gate â drop on different grid cell: move gate (reorder). 261 * C) Drag from placed gate â drop outside grid: remove gate (new behaviour). 262 * 263 * Case B issues a single executeCircuit call via _moveGate; no double-execution. 264 */ 265 endDrag(x, y) { 266 if (!this.isDragging) return; 267 268 const cell = this.getCellAtPosition(x, y); 269 270 if (this.dragSourceGate) { 271 // ---- Drag originated from a placed gate ---- 272 const src = this.dragSourceGate; 273 274 if (cell) { 275 const isSameCell = (cell.qubit === src.qubit && cell.time === src.time); 276 if (!isSameCell) { 277 // Case B: move gate to new position with a single executeCircuit 278 this._moveGate(src.qubit, src.time, cell.qubit, cell.time); 279 } else { 280 // Same cell: if movement was negligible treat as a tap â open angle editor 281 const dist = Math.hypot(x - this.dragStartPos.x, y - this.dragStartPos.y); 282 if (dist < 6) { 283 const gateData = this.composer.circuit.find( 284 g => g.qubit === src.qubit && g.time === src.time 285 ); 286 if (gateData && this.composer.gates.hasParameters(gateData.type)) { 287 // Defer so cleanupDrag() runs first and restores the element 288 setTimeout(() => this.openAngleEditor(gateData), 0); 289 } 290 } 291 } 292 } else { 293 // Case C: dropped outside circuit â delete the gate 294 this.composer.removeGate(src.qubit, src.time); 295 } 296 } else if (cell && this.dragGateType) { 297 // Case A: toolbar â grid (original behaviour) 298 this.placeGate(cell.qubit, cell.time, this.dragGateType); 299 } 300 301 this.cleanupDrag(); 302 } 303 304 /** 305 * Move a gate from (srcQubit, srcTime) to (destQubit, destTime) in a single 306 * atomic operation that calls executeCircuit exactly once. 307 * If the destination is occupied, that gate is displaced/removed first. 308 */ 309 _moveGate(srcQubit, srcTime, destQubit, destTime) { 310 // Grab full gate data before modifying the circuit array 311 const gateData = this.composer.circuit.find( 312 g => g.qubit === srcQubit && g.time === srcTime 313 ); 314 if (!gateData) return; 315 316 // Remove source visual 317 this.composer.grid.removeGateVisual(srcQubit, srcTime); 318 319 // If destination is occupied, evict the occupying gate 320 const occupier = this.composer.circuit.find( 321 g => g.qubit === destQubit && g.time === destTime 322 ); 323 if (occupier) { 324 this.composer.grid.removeGateVisual(destQubit, destTime); 325 this.composer.circuit = this.composer.circuit.filter( 326 g => !(g.qubit === destQubit && g.time === destTime) 327 ); 328 } 329 330 // Remove source entry from circuit array 331 this.composer.circuit = this.composer.circuit.filter( 332 g => !(g.qubit === srcQubit && g.time === srcTime) 333 ); 334 335 // Update gate coordinates in-place and re-insert 336 gateData.qubit = destQubit; 337 gateData.time = destTime; 338 this.composer.circuit.push(gateData); 339 340 // Re-render the gate visual at its new position 341 this.composer.grid.addGateVisual(gateData); 342 343 // Single state recomputation 344 this.composer.executeCircuit(); 345 } 346 347 /** 348 * Get cell at mouse position 349 */ 350 getCellAtPosition(x, y) { 351 const element = document.elementFromPoint(x, y); 352 const cell = element?.closest('.grid-cell[data-qubit][data-time]'); 353 354 if (!cell) return null; 355 356 return { 357 qubit: parseInt(cell.getAttribute('data-qubit')), 358 time: parseInt(cell.getAttribute('data-time')) 359 }; 360 } 361 362 /** 363 * Place gate at specified position 364 */ 365 placeGate(qubit, time, gateType) { 366 // Check if cell is already occupied 367 const existingGate = this.composer.circuit.find(gate => 368 gate.qubit === qubit && gate.time === time 369 ); 370 371 if (existingGate) { 372 // Replace existing gate 373 this.composer.removeGate(qubit, time); 374 } 375 376 // Check if gate needs parameters 377 const definition = this.composer.gates.getGateDefinition(gateType); 378 379 if (definition.hasParameter) { 380 // Place immediately with a 90° default; user can tap the placed gate to edit the angle 381 const params = this.composer.gates.getDefaultParameters(gateType, qubit, this.composer.getQubitCount()); 382 this.composer.addGate(gateType, qubit, time, params); 383 } else if (definition.isMultiQubit) { 384 // Add multi-qubit gates directly with standard default adjacent tracks 385 const params = this.composer.gates.getDefaultParameters(gateType, qubit, this.composer.getQubitCount()); 386 this.composer.addGate(gateType, qubit, time, params); 387 } else { 388 // Add single qubit gates directly 389 this.composer.addGate(gateType, qubit, time); 390 } 391 } 392 393 /** 394 * Clean up drag operation 395 */ 396 cleanupDrag() { 397 // Remove drag ghost 398 if (this.dragElement) { 399 this.dragElement.remove(); 400 this.dragElement = null; 401 } 402 403 // Remove dragging classes from toolbar buttons 404 document.querySelectorAll('.gate-tool-btn.dragging').forEach(btn => { 405 btn.classList.remove('dragging'); 406 }); 407 408 document.body.classList.remove('circuit-dragging'); 409 410 // Restore the source gate element that was dimmed during a grid drag. 411 // If the gate was deleted, the element has already been removed from the 412 // DOM by removeGate â removeGateVisual, so the style reset is a no-op. 413 if (this._dragSourceElement) { 414 this._dragSourceElement.style.opacity = ''; 415 this._dragSourceElement.style.pointerEvents = ''; 416 this._dragSourceElement = null; 417 } 418 419 // Remove hover highlight 420 if (this.currentHoverCell) { 421 this.composer.grid.removeHighlight( 422 this.currentHoverCell.qubit, 423 this.currentHoverCell.time 424 ); 425 this.currentHoverCell = null; 426 } 427 428 // Reset all state 429 this.isDragging = false;
430 this.dragGateType = null; 431 this.dragSourceGate = null; 432 this.dragStartPos = { x: 0, y: 0 }; 433 } 434 435 /** 436 * Handle click on grid cells for direct placement 437 */ 438 handleGridClick(e) { 439 const cell = e.target.closest('.grid-cell[data-qubit][data-time]'); 440 if (!cell) return; 441 442 const qubit = parseInt(cell.getAttribute('data-qubit')); 443 const time = parseInt(cell.getAttribute('data-time')); 444 445 // If a gate is selected, place it 446 if (this.composer.selectedGate) { 447 this.placeGate(qubit, time, this.composer.selectedGate); 448 } 449 } 450 451 /** 452 * Handle keyboard shortcuts 453 */ 454 handleKeyDown(e) { 455 // Escape to cancel drag 456 if (e.key === 'Escape' && this.isDragging) { 457 this.cleanupDrag(); 458 } 459 460 // Delete to remove selected gate 461 if (e.key === 'Delete' || e.key === 'Backspace') { 462 const selectedGate = document.querySelector('.gate-element.selected'); 463 if (selectedGate) { 464 const qubit = parseInt(selectedGate.getAttribute('data-qubit')); 465 const time = parseInt(selectedGate.getAttribute('data-time')); 466 this.composer.removeGate(qubit, time); 467 } 468 } 469 } 470 471 /** 472 * Initialize grid click handlers 473 */ 474 initGridClickHandlers() { 475 const grid = document.getElementById('circuit-grid'); 476 if (grid) { 477 grid.addEventListener('click', (e) => this.handleGridClick(e)); 478 } 479 } 480 481 /** 482 * Open a compact angle-editor popup anchored below a placed rotation gate. 483 * Called when the user taps (clicks without dragging) a placed RX/RY/RZ gate. 484 */ 485 openAngleEditor(gateData) { 486 // Remove any stale popup 487 const existing = document.getElementById('angle-editor-popup'); 488 if (existing) existing.remove(); 489 490 const currentAngle = (gateData.params && gateData.params.angle != null) 491 ? gateData.params.angle : 90; 492 493 const popup = document.createElement('div'); 494 popup.id = 'angle-editor-popup'; 495 popup.className = 'angle-editor-popup'; 496 popup.innerHTML = ` 497 <div class="angle-editor-title">${gateData.type} â set angle</div> 498 <div class="angle-editor-row"> 499 <input type="number" class="angle-editor-input" id="angle-editor-input" 500 min="0" max="360" step="1" value="${currentAngle}"> 501 <span class="angle-editor-unit">°</span> 502 </div> 503 <div class="angle-editor-actions"> 504 <button class="angle-editor-btn" id="angle-editor-cancel">Cancel</button> 505 <button class="angle-editor-btn primary" id="angle-editor-apply">Set</button> 506 </div> 507 `; 508 509 document.body.appendChild(popup); 510 511 // Position popup below the gate element 512 const gateEl = document.querySelector( 513 `.gate-element[data-qubit="${gateData.qubit}"][data-time="${gateData.time}"]` 514 ); 515 if (gateEl) { 516 const rect = gateEl.getBoundingClientRect(); 517 const popupW = 180; 518 let left = rect.left + rect.width / 2 - popupW / 2; 519 left = Math.max(8, Math.min(left, window.innerWidth - popupW - 8)); 520 popup.style.left = left + 'px'; 521 popup.style.top = (rect.bottom + 6) + 'px'; 522 } 523 524 const inputEl = popup.querySelector('#angle-editor-input'); 525 inputEl.focus(); 526 inputEl.select(); 527 528 const apply = () => { 529 const raw = parseFloat(inputEl.value); 530 if (!isNaN(raw) && raw >= 0 && raw <= 360) { 531 gateData.params.angle = raw; 532 // Refresh the gate label in the grid 533 const el = document.querySelector( 534 `.gate-element[data-qubit="${gateData.qubit}"][data-time="${gateData.time}"]` 535 ); 536 if (el) el.textContent = `${gateData.type}(${raw}°)`; 537 this.composer.executeCircuit(); 538 } 539 popup.remove(); 540 }; 541 542 popup.querySelector('#angle-editor-apply').addEventListener('click', apply); 543 popup.querySelector('#angle-editor-cancel').addEventListener('click', () => popup.remove()); 544 545 // Confirm with Enter, dismiss with Escape 546 inputEl.addEventListener('keydown', (e) => { 547 if (e.key === 'Enter') apply(); 548 if (e.key === 'Escape') popup.remove(); 549 }); 550 551 // Dismiss when clicking outside the popup 552 const onOutside = (e) => { 553 if (!popup.contains(e.target)) { 554 popup.remove(); 555 document.removeEventListener('mousedown', onOutside, true); 556 } 557 }; 558 // Use capture so it fires before other handlers; small delay to avoid 559 // the same click that opened the popup immediately closing it. 560 setTimeout(() => document.addEventListener('mousedown', onOutside, true), 50); 561 } 562 563 /** 564 * Initialize keyboard handlers 565 */ 566 initKeyboardHandlers() { 567 document.addEventListener('keydown', (e) => this.handleKeyDown(e)); 568 } 569 570 /** 571 * Enable drag and drop 572 */ 573 enable() { 574 this.initGridClickHandlers(); 575 this.initKeyboardHandlers(); 576 } 577 578 /** 579 * Disable drag and drop 580 */ 581 disable() { 582 if (this.isDragging) { 583 this.cleanupDrag(); 584 } 585 } 586}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.