PageSourceSearch

https://bloqqer.onrender.com/js/composer/dragHandler.js

js bloqqer.onrender.com collected 2026-10-03 11:39:24 UTC 21,453 bytes, 586 lines download raw bytes

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.