145 lines
5.2 KiB
JavaScript
145 lines
5.2 KiB
JavaScript
|
|
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);
|
||
|
|
};
|
||
|
|
|
||
|
|
const applySelectionUI = () => {
|
||
|
|
const rowNodes = container.querySelectorAll('.dt-body-row');
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
rowNodes.forEach((rowNode, r) => {
|
||
|
|
const absoluteRow = r + state._listStartIndex;
|
||
|
|
const cellNodes = rowNode.children;
|
||
|
|
|
||
|
|
// Fast path: Row completely outside selection
|
||
|
|
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
||
|
|
for (let i = 0; i < cellNodes.length; i++) {
|
||
|
|
if (cellNodes[i].classList.contains('dt-cell-selected')) {
|
||
|
|
cellNodes[i].classList.remove('dt-cell-selected');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let i = 0; i < cellNodes.length; i++) {
|
||
|
|
const isSelected = isCellSelected(absoluteRow, i);
|
||
|
|
const hasClass = cellNodes[i].classList.contains('dt-cell-selected');
|
||
|
|
if (isSelected && !hasClass) {
|
||
|
|
cellNodes[i].classList.add('dt-cell-selected');
|
||
|
|
} else if (!isSelected && hasClass) {
|
||
|
|
cellNodes[i].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);
|
||
|
|
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) {
|
||
|
|
if (!e.ctrlKey && !e.metaKey) {
|
||
|
|
clearAllActive();
|
||
|
|
} else if (activeBounds) {
|
||
|
|
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)
|
||
|
|
};
|
||
|
|
applySelectionUI();
|
||
|
|
updateStatus();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const endSelect = () => { 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 };
|
||
|
|
};
|
||
|
|
|
||
|
|
const deleteSelected = () => {
|
||
|
|
const bounds = getSelectionBounds();
|
||
|
|
if (!bounds) return;
|
||
|
|
|
||
|
|
// This is a naive deletion that removes the bounding box rows entirely.
|
||
|
|
// A complete implementation might filter row by row checking multi-selections.
|
||
|
|
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
|
||
|
|
};
|
||
|
|
};
|