export const createSelectionManager = (container, state) => { let activeBounds = null; // { minRow, maxRow, minCol, maxCol } let startCell = null; // { row, col } let multiSelections = []; const isCellSelected = (r, c) => { if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true; return multiSelections.some(s => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol); }; let lastHadSelection = false; const applySelectionUI = () => { /* DEBUG: Performance Isolation Toggle */ if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return; let boundMinRow = Infinity, boundMaxRow = -Infinity; if (activeBounds) { boundMinRow = Math.min(boundMinRow, activeBounds.minRow); boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow); } multiSelections.forEach(s => { boundMinRow = Math.min(boundMinRow, s.minRow); boundMaxRow = Math.max(boundMaxRow, s.maxRow); }); const hasSelection = boundMinRow !== Infinity; if (!hasSelection && !lastHadSelection) return; lastHadSelection = hasSelection; const body = container.querySelector('.dt-body'); if (!body) return; const rowNodes = body.querySelectorAll('.dt-body-row'); rowNodes.forEach(rowNode => { const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex; const cells = rowNode.querySelectorAll('.dt-cell'); if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) { cells.forEach(cell => cell.classList.remove('dt-cell-selected')); return; } cells.forEach((cell, cIdx) => { if (isCellSelected(absoluteRow, cIdx)) { cell.classList.add('dt-cell-selected'); } else { cell.classList.remove('dt-cell-selected'); } }); }); }; const updateStatus = () => { let count = 0; if (activeBounds) count += (activeBounds.maxRow - activeBounds.minRow + 1); multiSelections.forEach(s => count += (s.maxRow - s.minRow + 1)); state.selectedRowCount = count; }; const clearAllActive = (keepSelection = false) => { if (!keepSelection) { activeBounds = null; startCell = null; multiSelections = []; applySelectionUI(); updateStatus(); } }; const startSelect = (row, col, e) => { const alreadySelected = isCellSelected(row, col); const isRange = (activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol)) || multiSelections.length > 0; if (e.shiftKey && startCell) { activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) }; } else { if (alreadySelected && !e.ctrlKey && !e.metaKey) { // If clicking inside a range or a single selection, don't clear yet. // Mark for potential single-cell toggle-off on mouseup if it was just a single cell. if (!isRange) container._potentialCancel = { row, col }; // else: Keep current selection as is (sticky selection) } else { if (!e.ctrlKey && !e.metaKey) { clearAllActive(); } else if (activeBounds && !alreadySelected) { multiSelections.push(activeBounds); } startCell = { row, col }; activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col }; } state.isSelecting = true; } applySelectionUI(); updateStatus(); container.focus(); }; const updateSelect = (row, col) => { if (state.isSelecting && startCell) { activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) }; container._potentialCancel = null; // Dragging means no cancel applySelectionUI(); updateStatus(); } }; const endSelect = () => { if (container._potentialCancel) { const { row, col } = container._potentialCancel; if (isCellSelected(row, col)) { clearAllActive(); } container._potentialCancel = null; } state.isSelecting = false; }; const getSelectionBounds = () => { if (!activeBounds) return null; let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow; let minCol = activeBounds.minCol, maxCol = activeBounds.maxCol; multiSelections.forEach(s => { minRow = Math.min(minRow, s.minRow); maxRow = Math.max(maxRow, s.maxRow); minCol = Math.min(minCol, s.minCol); maxCol = Math.max(maxCol, s.maxCol); }); return { minRow, maxRow, minCol, maxCol }; }; // Excel-style TSV Copy const copy = async () => { const bounds = getSelectionBounds(); if (!bounds) return; const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map(row => { return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map(f => { let val = String(row[f.id] ?? ''); if (val.includes('\t') || val.includes('\n') || val.includes('"')) { val = '"' + val.replace(/"/g, '""') + '"'; } return val; }).join('\t'); }).join('\n'); await navigator.clipboard.writeText(text); }; // Excel-style TSV Paste (Optimized for targeted refresh) const paste = async () => { try { const text = await navigator.clipboard.readText(); if (!text) return; const bounds = getSelectionBounds(); if (!bounds) return; // Simple TSV parser const rows = text.split(/\r?\n/).filter(line => line.length > 0).map(line => { const cells = []; let current = '', inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i+1] === '"') { current += '"'; i++; } else inQuotes = !inQuotes; } else if (char === '\t' && !inQuotes) { cells.push(current); current = ''; } else { current += char; } } cells.push(current); return cells; }); const startRow = bounds.minRow; const startCol = bounds.minCol; const maxRow = bounds.maxRow; const maxCol = bounds.maxCol; const body = container.querySelector('.dt-body'); const rowNodes = body ? Array.from(body.childNodes).filter(n => n.classList?.contains('dt-body-row')) : []; rows.forEach((rowData, rOffset) => { const rIdx = startRow + rOffset; if (rIdx > maxRow || rIdx >= state.list.length) return; const rowItem = state.list[rIdx]; let rowChanged = false; rowData.forEach((cellData, cOffset) => { const cIdx = startCol + cOffset; if (cIdx > maxCol || cIdx >= state.fields.length) return; const field = state.fields[cIdx]; rowItem[field.id] = cellData; rowChanged = true; }); // Targeted DOM refresh to avoid full list scan lag if (rowChanged && container.refreshNode) { const domNode = rowNodes.find(n => (n._ref?.rIdx ?? -1) + state._listStartIndex === rIdx); if (domNode) container.refreshNode(domNode); } }); } catch (err) { console.error('Paste Error:', err); } }; const deleteSelected = () => { const bounds = getSelectionBounds(); if (!bounds) return; const rMin = bounds.minRow, rMax = bounds.maxRow; const newList = state.list.filter((_, i) => !(i >= rMin && i <= rMax)); state.list = newList; clearAllActive(); }; return { applySelectionUI, clearAllActive, startSelect, updateSelect, endSelect, deleteSelected, getSelectionBounds, copy, paste }; };