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 }