selection.js (3.9 KB)
1 // Marquee multi-select: drag a box over empty grid to select images, then move them as a unit 2 3 let selectedImages = new Set(); 4 let marqueeState = null; 5 const MARQUEE_FADE_MS = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--marquee-fade')); 6 7 const selectionBox = document.createElement('div'); 8 selectionBox.className = 'selection-box'; 9 grid.appendChild(selectionBox); 10 11 function isSelected(imageData) { 12 return selectedImages.has(imageData); 13 } 14 15 function setSelection(imageList) { 16 clearSelection(); 17 imageList.forEach(img => { 18 selectedImages.add(img); 19 img.container.classList.add('selected'); 20 }); 21 } 22 23 function clearSelection() { 24 selectedImages.forEach(img => img.container.classList.remove('selected')); 25 selectedImages.clear(); 26 } 27 28 // Snap a client point to the nearest cell boundary (0..GRID_COLS / 0..GRID_ROWS) 29 function pointToBoundary(clientX, clientY) { 30 const gridRect = grid.getBoundingClientRect(); 31 const cellSize = getCellSize(); 32 return { 33 col: Math.max(0, Math.min(GRID_COLS, Math.round((clientX - gridRect.left) / cellSize.width))), 34 row: Math.max(0, Math.min(GRID_ROWS, Math.round((clientY - gridRect.top) / cellSize.height))) 35 }; 36 } 37 38 function marqueeRect() { 39 return { 40 x0: Math.min(marqueeState.anchorCol, marqueeState.col), 41 y0: Math.min(marqueeState.anchorRow, marqueeState.row), 42 x1: Math.max(marqueeState.anchorCol, marqueeState.col), 43 y1: Math.max(marqueeState.anchorRow, marqueeState.row) 44 }; 45 } 46 47 function updateMarquee() { 48 const { x0, y0, x1, y1 } = marqueeRect(); 49 const widthCells = x1 - x0; 50 const heightCells = y1 - y0; 51 52 if (widthCells === 0 || heightCells === 0) { 53 selectionBox.style.display = 'none'; 54 clearSelection(); 55 return; 56 } 57 58 const bounds = getPixelPerfectBounds(x0, y0, widthCells, heightCells); 59 selectionBox.style.display = 'block'; 60 selectionBox.style.left = bounds.left + 'px'; 61 selectionBox.style.top = bounds.top + 'px'; 62 selectionBox.style.width = bounds.width + 'px'; 63 selectionBox.style.height = bounds.height + 'px'; 64 65 setSelection(images.filter(img => 66 img.xCell < x1 && img.xCell + img.widthCells > x0 && 67 img.yCell < y1 && img.yCell + img.heightCells > y0 68 )); 69 } 70 71 grid.addEventListener('mousedown', (e) => { 72 if (e.button !== 0) return; 73 if (e.target.closest('.image-container')) return; 74 75 e.preventDefault(); 76 clearSelection(); 77 78 const { col, row } = pointToBoundary(e.clientX, e.clientY); 79 marqueeState = { anchorCol: col, anchorRow: row, col, row }; 80 document.body.classList.add('selecting'); 81 }); 82 83 function handleMarqueeMove(clientX, clientY) { 84 if (!marqueeState) return; 85 const { col, row } = pointToBoundary(clientX, clientY); 86 if (col === marqueeState.col && row === marqueeState.row) return; 87 marqueeState.col = col; 88 marqueeState.row = row; 89 updateMarquee(); 90 } 91 92 function fadeOutMarquee() { 93 const ghost = selectionBox.cloneNode(); 94 ghost.classList.add('releasing'); 95 grid.appendChild(ghost); 96 setTimeout(() => ghost.remove(), MARQUEE_FADE_MS + 50); 97 } 98 99 function endMarquee() { 100 if (!marqueeState) return; 101 marqueeState = null; 102 document.body.classList.remove('selecting'); 103 104 if (selectionBox.style.display === 'none') return; 105 fadeOutMarquee(); 106 selectionBox.style.display = 'none'; 107 } 108 109 document.addEventListener('mousemove', (e) => handleMarqueeMove(e.clientX, e.clientY)); 110 document.addEventListener('mouseup', endMarquee); 111 112 // Delete/Backspace deletes all selected images (same as shift+click) 113 document.addEventListener('keydown', (e) => { 114 if (e.key !== 'Delete' && e.key !== 'Backspace') return; 115 if (selectedImages.size === 0) return; 116 117 e.preventDefault(); 118 Array.from(selectedImages).forEach(deleteImage); 119 }); 120 121 // Bounds shared by group drag, duplicate, and rotate 122 function selectionBounds(group) { 123 return { 124 minX: Math.min(...group.map(img => img.xCell)), 125 minY: Math.min(...group.map(img => img.yCell)), 126 maxX: Math.max(...group.map(img => img.xCell + img.widthCells)), 127 maxY: Math.max(...group.map(img => img.yCell + img.heightCells)) 128 }; 129 }